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:
Zimin A.N.
2026-05-16 13:55:16 +03:00
parent db5d5c19e2
commit e4cd7b9709
10 changed files with 861 additions and 1 deletions
+36
View File
@@ -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',