АДРЕСНЫЙ РЕЖИМ - локальная подель на декомпозе

This commit is contained in:
2026-04-01 17:55:02 +03:00
parent 4060a5e575
commit 4d59672576
90 changed files with 19595 additions and 785 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NDC AI Normalizer Playground</title>
<script type="module" crossorigin src="/assets/index-B5_Zqbf2.js"></script>
<script type="module" crossorigin src="/assets/index-BFy6DcyX.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Ch7jCAii.css">
</head>
<body>
+57 -3
View File
@@ -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}
+26 -3
View File
@@ -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;