АДРЕСНЫЙ РЕЖИМ - локальная подель на декомпозе
This commit is contained in:
@@ -25,6 +25,7 @@ const SESSION_CONFIG_KEY = "ndc_normalizer_session_config_v1";
|
||||
const ASSISTANT_STAGES = ["Разбираю запрос", "Ищу данные", "Собираю ответ"];
|
||||
const DEFAULT_UI_MODE: UiMode = "assistant";
|
||||
const AUTOLOAD_PROMPT_VERSION = "normalizer_v2_0_2";
|
||||
const ASSISTANT_PROMPT_VERSION = "address_query_runtime_v1";
|
||||
|
||||
function withTs(message: string): string {
|
||||
return `[${new Date().toLocaleTimeString("ru-RU")}] ${message}`;
|
||||
@@ -49,6 +50,8 @@ export default function App() {
|
||||
const [appLogs, setAppLogs] = useState<string[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<TabKey>("normalized");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [modelsBusy, setModelsBusy] = useState(false);
|
||||
const [modelOptions, setModelOptions] = useState<string[]>([]);
|
||||
const [connectionStatus, setConnectionStatus] = useState("");
|
||||
const [presetList, setPresetList] = useState<
|
||||
Array<{
|
||||
@@ -104,6 +107,7 @@ export default function App() {
|
||||
const parsed = JSON.parse(cached) as Partial<ConnectionState>;
|
||||
setConnection((prev) => ({
|
||||
...prev,
|
||||
llmProvider: parsed.llmProvider === "local" ? "local" : "openai",
|
||||
model: parsed.model ?? prev.model,
|
||||
baseUrl: parsed.baseUrl ?? prev.baseUrl,
|
||||
temperature: parsed.temperature ?? prev.temperature,
|
||||
@@ -174,6 +178,7 @@ export default function App() {
|
||||
SESSION_CONFIG_KEY,
|
||||
JSON.stringify({
|
||||
model: connection.model,
|
||||
llmProvider: connection.llmProvider,
|
||||
baseUrl: connection.baseUrl,
|
||||
temperature: connection.temperature,
|
||||
maxOutputTokens: connection.maxOutputTokens
|
||||
@@ -187,8 +192,24 @@ export default function App() {
|
||||
setLastError("");
|
||||
try {
|
||||
const payload = await apiClient.testConnection(connection);
|
||||
setConnectionStatus(`OK - ${payload.model}`);
|
||||
log(`OpenAI connection ok: ${payload.model}`);
|
||||
if (payload.provider === "local") {
|
||||
if (payload.model_found === true) {
|
||||
setConnectionStatus(`LOCAL OK - ${payload.model}`);
|
||||
log(`Local model is available: ${payload.model} (catalog size=${payload.models_count ?? "n/a"}).`);
|
||||
} else if (payload.model_found === false) {
|
||||
setConnectionStatus(`LOCAL OK, model not loaded - ${payload.model}`);
|
||||
log(
|
||||
`Local server is reachable, but model '${payload.model}' is not in loaded catalog. ` +
|
||||
`Use 'Load model list' and select one of loaded models.`
|
||||
);
|
||||
} else {
|
||||
setConnectionStatus(`LOCAL OK (model list unavailable) - ${payload.model}`);
|
||||
log("Local server is reachable, but model catalog could not be verified.");
|
||||
}
|
||||
} else {
|
||||
setConnectionStatus(`OPENAI OK - ${payload.model}`);
|
||||
log(`OpenAI connection ok: ${payload.model}`);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setConnectionStatus("Connection error");
|
||||
@@ -199,6 +220,33 @@ export default function App() {
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadModels() {
|
||||
setModelsBusy(true);
|
||||
try {
|
||||
const payload = await apiClient.listModels(connection);
|
||||
const models = payload.models ?? [];
|
||||
setModelOptions(models);
|
||||
if (models.length > 0) {
|
||||
setConnection((prev) => {
|
||||
if (prev.model && models.includes(prev.model)) {
|
||||
return prev;
|
||||
}
|
||||
return { ...prev, model: models[0] };
|
||||
});
|
||||
}
|
||||
log(`Model catalog loaded (${connection.llmProvider}): ${models.length} items.`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
log(`Load model list error: ${message}`);
|
||||
} finally {
|
||||
setModelsBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setModelOptions([]);
|
||||
}, [connection.llmProvider, connection.baseUrl]);
|
||||
|
||||
async function normalize(saveAsCase: boolean) {
|
||||
setBusy(true);
|
||||
setLastError("");
|
||||
@@ -451,7 +499,7 @@ export default function App() {
|
||||
prompts,
|
||||
userMessage,
|
||||
sessionId: assistantSessionId || undefined,
|
||||
promptVersion: "normalizer_v2_0_2",
|
||||
promptVersion: ASSISTANT_PROMPT_VERSION,
|
||||
context: {
|
||||
periodHint: query.periodHint,
|
||||
businessContext: query.businessContext
|
||||
@@ -504,7 +552,10 @@ export default function App() {
|
||||
<div className="layout-grid">
|
||||
<ConnectionPanel
|
||||
value={connection}
|
||||
modelOptions={modelOptions}
|
||||
modelsBusy={modelsBusy}
|
||||
onChange={setConnection}
|
||||
onReloadModels={reloadModels}
|
||||
onSaveLocalConfig={saveLocalConfig}
|
||||
onTestConnection={testConnection}
|
||||
lastStatus={connectionStatus}
|
||||
@@ -548,7 +599,10 @@ export default function App() {
|
||||
<div className="layout-grid">
|
||||
<ConnectionPanel
|
||||
value={connection}
|
||||
modelOptions={modelOptions}
|
||||
modelsBusy={modelsBusy}
|
||||
onChange={setConnection}
|
||||
onReloadModels={reloadModels}
|
||||
onSaveLocalConfig={saveLocalConfig}
|
||||
onTestConnection={testConnection}
|
||||
lastStatus={connectionStatus}
|
||||
|
||||
@@ -27,10 +27,30 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
}
|
||||
|
||||
export const apiClient = {
|
||||
async testConnection(connection: ConnectionState): Promise<{ ok: boolean; model: string; timestamp: string }> {
|
||||
return request("/openai/test-connection", {
|
||||
async listModels(connection: ConnectionState): Promise<{ ok: boolean; models: string[]; count: number; timestamp: string }> {
|
||||
return request("/llm/models", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
llmProvider: connection.llmProvider,
|
||||
apiKey: connection.apiKey,
|
||||
model: connection.model,
|
||||
baseUrl: connection.baseUrl
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
async testConnection(connection: ConnectionState): Promise<{
|
||||
ok: boolean;
|
||||
provider: "openai" | "local";
|
||||
model: string;
|
||||
model_found: boolean | null;
|
||||
models_count: number | null;
|
||||
timestamp: string;
|
||||
}> {
|
||||
return request("/llm/test-connection", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
llmProvider: connection.llmProvider,
|
||||
apiKey: connection.apiKey,
|
||||
model: connection.model,
|
||||
baseUrl: connection.baseUrl
|
||||
@@ -54,6 +74,7 @@ export const apiClient = {
|
||||
return request("/normalize", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
llmProvider: params.connection.llmProvider,
|
||||
apiKey: params.connection.apiKey,
|
||||
model: params.connection.model,
|
||||
baseUrl: params.connection.baseUrl,
|
||||
@@ -130,6 +151,7 @@ export const apiClient = {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
normalizeConfig: {
|
||||
llmProvider: input.connection.llmProvider,
|
||||
apiKey: input.connection.apiKey,
|
||||
model: input.connection.model,
|
||||
baseUrl: input.connection.baseUrl,
|
||||
@@ -203,12 +225,13 @@ export const apiClient = {
|
||||
mode: "assistant",
|
||||
message: input.userMessage,
|
||||
user_message: input.userMessage,
|
||||
llmProvider: input.connection.llmProvider,
|
||||
apiKey: input.connection.apiKey,
|
||||
model: input.connection.model,
|
||||
baseUrl: input.connection.baseUrl,
|
||||
temperature: input.connection.temperature,
|
||||
maxOutputTokens: input.connection.maxOutputTokens,
|
||||
promptVersion: input.promptVersion ?? "normalizer_v2_0_2",
|
||||
promptVersion: input.promptVersion ?? "address_query_runtime_v1",
|
||||
systemPrompt: input.prompts.systemPrompt,
|
||||
developerPrompt: input.prompts.developerPrompt,
|
||||
domainPrompt: input.prompts.domainPrompt,
|
||||
|
||||
@@ -3,7 +3,10 @@ import type { ConnectionState } from "../state/types";
|
||||
|
||||
interface ConnectionPanelProps {
|
||||
value: ConnectionState;
|
||||
modelOptions: string[];
|
||||
modelsBusy: boolean;
|
||||
onChange: (next: ConnectionState) => void;
|
||||
onReloadModels: () => Promise<void> | void;
|
||||
onTestConnection: () => Promise<void> | void;
|
||||
onSaveLocalConfig: () => void;
|
||||
lastStatus: string;
|
||||
@@ -12,36 +15,94 @@ interface ConnectionPanelProps {
|
||||
|
||||
export function ConnectionPanel({
|
||||
value,
|
||||
modelOptions,
|
||||
modelsBusy,
|
||||
onChange,
|
||||
onReloadModels,
|
||||
onTestConnection,
|
||||
onSaveLocalConfig,
|
||||
lastStatus,
|
||||
busy
|
||||
}: ConnectionPanelProps) {
|
||||
const isLocal = value.llmProvider === "local";
|
||||
const modelInCatalog = modelOptions.includes(value.model);
|
||||
|
||||
return (
|
||||
<PanelFrame
|
||||
title="Подключение OpenAI"
|
||||
subtitle="Ключ живет только в памяти сессии (не пишется в localStorage)."
|
||||
actions={<span className="status-chip">{lastStatus || "Статус: не проверено"}</span>}
|
||||
title="LLM Connection"
|
||||
subtitle="Switch between OpenAI cloud and local OpenAI-compatible server."
|
||||
actions={<span className="status-chip">{lastStatus || "Status: not checked"}</span>}
|
||||
>
|
||||
<div className="grid-two">
|
||||
<label>
|
||||
OpenAI API Key
|
||||
Provider
|
||||
<select
|
||||
value={value.llmProvider}
|
||||
onChange={(event) => {
|
||||
const nextProvider = event.target.value === "local" ? "local" : "openai";
|
||||
onChange({
|
||||
...value,
|
||||
llmProvider: nextProvider,
|
||||
baseUrl: nextProvider === "local" ? "http://127.0.0.1:1234/v1" : "https://api.openai.com/v1"
|
||||
});
|
||||
}}
|
||||
>
|
||||
<option value="openai">OpenAI (token)</option>
|
||||
<option value="local">Local (LM Studio / OpenAI-compatible)</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Model
|
||||
<select
|
||||
value={modelInCatalog ? value.model : "__manual__"}
|
||||
onChange={(event) => {
|
||||
const selected = event.target.value;
|
||||
if (selected === "__manual__") {
|
||||
return;
|
||||
}
|
||||
onChange({ ...value, model: selected });
|
||||
}}
|
||||
>
|
||||
<option value="__manual__">Manual input</option>
|
||||
{modelOptions.map((modelId) => (
|
||||
<option key={modelId} value={modelId}>
|
||||
{modelId}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Model ID (manual)
|
||||
<input
|
||||
type="password"
|
||||
value={value.apiKey}
|
||||
onChange={(event) => onChange({ ...value, apiKey: event.target.value })}
|
||||
placeholder="sk-..."
|
||||
value={value.model}
|
||||
onChange={(event) => onChange({ ...value, model: event.target.value })}
|
||||
placeholder="qwen2.5-14b-instruct or lmstudio loaded model id"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Model ID
|
||||
<input value={value.model} onChange={(event) => onChange({ ...value, model: event.target.value })} />
|
||||
</label>
|
||||
<label>
|
||||
Base URL
|
||||
<input value={value.baseUrl} onChange={(event) => onChange({ ...value, baseUrl: event.target.value })} />
|
||||
|
||||
{!isLocal ? (
|
||||
<label className="full-width">
|
||||
OpenAI API Key
|
||||
<input
|
||||
type="password"
|
||||
value={value.apiKey}
|
||||
onChange={(event) => onChange({ ...value, apiKey: event.target.value })}
|
||||
placeholder="sk-..."
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
|
||||
<label className={isLocal ? "full-width" : undefined}>
|
||||
{isLocal ? "Local server base URL" : "Base URL"}
|
||||
<input
|
||||
value={value.baseUrl}
|
||||
onChange={(event) => onChange({ ...value, baseUrl: event.target.value })}
|
||||
placeholder={isLocal ? "http://127.0.0.1:1234/v1" : "https://api.openai.com/v1"}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Temperature
|
||||
<input
|
||||
@@ -51,6 +112,7 @@ export function ConnectionPanel({
|
||||
onChange={(event) => onChange({ ...value, temperature: Number(event.target.value) })}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Max output tokens
|
||||
<input
|
||||
@@ -60,12 +122,16 @@ export function ConnectionPanel({
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="button-row">
|
||||
<button type="button" onClick={() => onSaveLocalConfig()}>
|
||||
Сохранить локальную конфигурацию
|
||||
Save local config
|
||||
</button>
|
||||
<button type="button" onClick={() => onReloadModels()} disabled={busy || modelsBusy}>
|
||||
{modelsBusy ? "Loading models..." : "Load model list"}
|
||||
</button>
|
||||
<button type="button" onClick={() => onTestConnection()} disabled={busy}>
|
||||
{busy ? "Проверяем..." : "Проверить подключение"}
|
||||
{busy ? "Checking..." : "Test connection"}
|
||||
</button>
|
||||
</div>
|
||||
</PanelFrame>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ConnectionState, PromptState, QueryState } from "./types";
|
||||
|
||||
export const DEFAULT_CONNECTION: ConnectionState = {
|
||||
llmProvider: "openai",
|
||||
apiKey: "",
|
||||
model: "gpt-4o-mini",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export type TabKey = "normalized" | "fragments" | "scope" | "flags" | "route" | "raw" | "validation" | "logs";
|
||||
|
||||
export interface ConnectionState {
|
||||
llmProvider: "openai" | "local";
|
||||
apiKey: string;
|
||||
model: string;
|
||||
baseUrl: string;
|
||||
|
||||
Reference in New Issue
Block a user