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

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
+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;