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
+9
View File
@@ -794,6 +794,15 @@ export type SchemaTemplateDetail = SchemaTemplateSummary & {
schemaJson: unknown
}
/**
* AI Schema Assist suggest-field response. Shape должен matchить
* AiSchemaController output (parsed LLM JSON: {fieldName, schema}).
*/
export type AiFieldSuggestion = {
fieldName: string
schema: Record<string, unknown>
}
/**
* Empty-state hint payload (read-api scheduled-summary). Подсчёт записей с
* {@code validFrom > now AND validTo > now} в текущем scope view.
+26
View File
@@ -2,6 +2,7 @@ import { useCallback, useRef } from 'react'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import {
apiClient,
type AiFieldSuggestion,
type BulkCloseRequest,
type BulkCloseResponse,
type CascadeCloseResult,
@@ -741,3 +742,28 @@ export const useUpdateNotificationPreferences = () => {
},
})
}
/**
* AI Schema Assist — per-field suggest. Returns parsed {fieldName, schema}.
* Каллер показывает diff preview, user accept/edit/reject.
*
* 404 → AI disabled на бэкенде — frontend hides button (handled в caller).
* 503/circuit_open → temporary unavailable, show banner.
* 422 → bad output from LLM — show error message, suggest retry.
* 429 → rate limit, show "try again in a minute".
*/
export const useAiSuggestField = () => {
return useMutation({
mutationFn: async (req: {
existingSchema: unknown
prompt: string
}): Promise<AiFieldSuggestion> => {
const { data } = await apiClient.post<AiFieldSuggestion>(
'/ai/suggest-field',
req,
{ timeout: 30_000 }, // override default 10s — LLM может быть slow
)
return data
},
})
}
+28
View File
@@ -852,6 +852,34 @@ export const schemaTemplateDetailQuery = (id: string) =>
},
staleTime: 5 * 60_000,
})
// ─────────────────────────────────────────────────────────────────────────────
// AI feature detection — probe endpoint
// ─────────────────────────────────────────────────────────────────────────────
/**
* Probe AI Schema Assist availability. Backend регистрирует /ai/suggest-field
* conditionally на ordinis.ai.enabled — если выключено, OPTIONS возвращает 404.
*
* <p>Used UI чтобы decide показывать ли «AI suggest» button. Cache 5 min —
* feature flag меняется только при helm upgrade.
*/
export const aiFeatureAvailableQuery = queryOptions({
queryKey: ['ai-feature-available'] as const,
queryFn: async (): Promise<boolean> => {
try {
const { data } = await apiClient.get<{ enabled: boolean }>('/ai/info')
return Boolean(data?.enabled)
} catch {
// 404 (AI disabled) или network error → hide button (graceful default).
return false
}
},
staleTime: 5 * 60_000,
retry: false,
})
export const useAiFeatureAvailable = () => useQuery(aiFeatureAvailableQuery)
export const useRecordRaw = (
dictionaryName: string,
businessKey: string | undefined,