feat(ai): Phase 1 step 3-6 — LLM adapter + per-field AI suggest endpoint + UI
Approach C upgrade на templates foundation (!233). Admin может попросить AI предложить новое поле — system prompt force'ит structural JSON Schema fragment, admin review через accept/edit/reject. Bitemporal draft workflow unchanged downstream. ## Backend - `LlmAdapter` — OpenAI-compatible /v1/chat/completions client (Ollama, vLLM, OpenAI). Bean conditional на ordinis.ai.enabled=true. - `AiSchemaService` — system prompt + 3 few-shot examples, JSON output parsing (с markdown fence stripping), validation (fieldName snake_case). In-memory circuit breaker: 10 fails в 60s → open 5 min. - `AiSchemaController`: - GET /api/v1/ai/info → 200 (enabled) / 404 (disabled). Frontend probe. - POST /api/v1/ai/suggest-field → suggestion. Per-IP rate limit 30/min. - All bean-conditional: ordinis.ai.enabled=false → controllers нет → endpoints 404 → frontend hides AI buttons automatically. ## Frontend - `useAiFeatureAvailable` query — probes /ai/info, cache 5 min - `useAiSuggestField` mutation — 30s timeout (LLM может быть slow) - `AiFieldSuggestPanel` component — inline expandable: prompt → preview → accept/retry/reject. Error handling per HTTP status (503/429/422/502/404). - DictionaryEditorDialog: AI toggle visible на schema tab когда aiAvailable=true. Accept wraps suggestion в mini-schema → parseSchemaJson (reuse existing kind-inference) → push в properties (overwrite если duplicate fieldName). - i18n RU + EN ## Config - application.yml: ordinis.ai.{enabled,endpoint,model,bearer-token, timeout-seconds} - Helm writer.yaml: ORDINIS_AI_* env vars, optional secretRef для bearer - values.yaml: ai.{enabled,endpoint,model,timeoutSeconds,bearerTokenSecretRef} defaults disabled Pair с infra MR — values-staging enables AI с vortex.nstart.cloud Ollama serving qwen2.5:7b.
This commit is contained in:
@@ -370,6 +370,24 @@ i18n
|
||||
'dict.scheduled.cta': 'Перейти к этой дате',
|
||||
'dict.scheduled.row.badge': 'Запланировано',
|
||||
'dict.scheduled.row.tooltip': 'У записи есть запланированная будущая версия',
|
||||
'aiSuggest.openButton': 'AI: добавить поле',
|
||||
'aiSuggest.title': 'AI: добавить поле',
|
||||
'aiSuggest.placeholder': 'Опиши поле — например «орбита: апогей, перигей, наклонение»',
|
||||
'aiSuggest.hint': 'AI предложит структуру (type/format/x-references). GOST коды и validations добавь сам.',
|
||||
'aiSuggest.suggest': 'Предложить',
|
||||
'aiSuggest.thinking': 'Думаю…',
|
||||
'aiSuggest.preview': 'Предложение',
|
||||
'aiSuggest.retry': 'Перегенерировать',
|
||||
'aiSuggest.accept': 'Добавить в схему',
|
||||
'aiSuggest.error.circuit': 'AI временно недоступен. Попробуй через несколько минут.',
|
||||
'aiSuggest.error.rate': 'Слишком много запросов. Подожди минуту.',
|
||||
'aiSuggest.error.bad_output': 'AI вернул невалидный ответ. Попробуй переформулировать.',
|
||||
'aiSuggest.error.upstream': 'LLM endpoint недоступен. Жди restore или попробуй позже.',
|
||||
'aiSuggest.error.disabled': 'AI отключён в этой инсталляции.',
|
||||
'aiSuggest.error.generic': 'Не удалось получить предложение.',
|
||||
'schemaTemplates.label': 'Начать с шаблона',
|
||||
'schemaTemplates.hint': 'Шаблон загрузит начальные поля. Дополни их вручную.',
|
||||
'common.close': 'Закрыть',
|
||||
'dict.col.businessKey': 'Бизнес-ключ',
|
||||
'dict.col.scope': 'Scope',
|
||||
'dict.col.validFrom': 'Действует с',
|
||||
@@ -1189,6 +1207,24 @@ i18n
|
||||
'dict.scheduled.cta': 'Jump to that date',
|
||||
'dict.scheduled.row.badge': 'Scheduled',
|
||||
'dict.scheduled.row.tooltip': 'This record has an upcoming scheduled version',
|
||||
'aiSuggest.openButton': 'AI: add field',
|
||||
'aiSuggest.title': 'AI: add field',
|
||||
'aiSuggest.placeholder': 'Describe the field — e.g. "orbit: apogee, perigee, inclination"',
|
||||
'aiSuggest.hint': 'AI suggests structure (type/format/x-references). Add GOST codes and validations manually.',
|
||||
'aiSuggest.suggest': 'Suggest',
|
||||
'aiSuggest.thinking': 'Thinking…',
|
||||
'aiSuggest.preview': 'Suggestion',
|
||||
'aiSuggest.retry': 'Regenerate',
|
||||
'aiSuggest.accept': 'Add to schema',
|
||||
'aiSuggest.error.circuit': 'AI temporarily unavailable. Try again in a few minutes.',
|
||||
'aiSuggest.error.rate': 'Too many requests. Wait a minute.',
|
||||
'aiSuggest.error.bad_output': 'AI returned invalid response. Try rephrasing.',
|
||||
'aiSuggest.error.upstream': 'LLM endpoint unreachable. Wait or try later.',
|
||||
'aiSuggest.error.disabled': 'AI is disabled in this installation.',
|
||||
'aiSuggest.error.generic': 'Failed to get suggestion.',
|
||||
'schemaTemplates.label': 'Start from template',
|
||||
'schemaTemplates.hint': 'Template loads initial fields. Extend manually.',
|
||||
'common.close': 'Close',
|
||||
'dict.col.businessKey': 'Business key',
|
||||
'dict.col.scope': 'Scope',
|
||||
'dict.col.validFrom': 'Valid from',
|
||||
|
||||
Reference in New Issue
Block a user