AI-rutingskontroll i admin: 13 kontekster konfigurerbare uten redeploy (oppgave 28.2)
Utvider /admin/ai med full kontroll over hvilken modellalias som brukes per AI-kontekst. Admin kan bytte modell for orkestrering, bot-chat, oppsummering, edge-forslag, klassifisering osv. uten å restarte maskinrommet. Endringer: - Migration 028: seeder 7 nye kontekster i ai_job_routing (orchestration_script/dream, bot_chat/triage, summarize, suggest_edges, classify) - Backend: resolve_routing_or_default() i ai_admin.rs — felles oppslag mot ai_job_routing med fallback til sidelinja/rutine - Dispatchers (ai_edges, summarize) bruker nå routing-tabellen i stedet for hardkodede env-variabler — endringer trer i kraft umiddelbart - Frontend: Ruting-tab omskrevet med kategoriserte kontekster (Orkestrering, Bot & chat, Analyse, Prosessering), beskrivelser per kontekst, og støtte for egendefinerte regler - Docs: ai_gateway.md §3.4 oppdatert med alle 13 kontekster
This commit is contained in:
parent
1c59604b07
commit
a06b79478a
7 changed files with 257 additions and 59 deletions
|
|
@ -78,17 +78,47 @@ Generert config inkluderer alltid `router_settings` og `general_settings` fra fa
|
|||
|
||||
### 3.4 Jobbkø-styrt modellvalg
|
||||
|
||||
Jobbkøen bruker `ai_job_routing` for å bestemme modellalias per jobbtype:
|
||||
Jobbkøen bruker `ai_job_routing` for å bestemme modellalias per kontekst.
|
||||
Admin kan endre mapping i `/admin/ai` uten redeploy — endringer trer i kraft
|
||||
ved neste jobb som kjøres.
|
||||
|
||||
| Jobbtype | Standard alias | Begrunnelse |
|
||||
**Orkestrering:**
|
||||
|
||||
| Kontekst | Standard alias | Begrunnelse |
|
||||
|---|---|---|
|
||||
| `ai_text_process` (✨-behandling) | `sidelinja/rutine` | Tekstvasking, høyt volum |
|
||||
| `orchestration_script` | `sidelinja/rutine` | LLM-kall i orkestreringsskript (SPØR/TRANSFORMER-steg) |
|
||||
| `orchestration_dream` | `sidelinja/resonering` | Kreativ/utforskende orkestrering — drømmemodus |
|
||||
|
||||
**Bot & chat:**
|
||||
|
||||
| Kontekst | Standard alias | Begrunnelse |
|
||||
|---|---|---|
|
||||
| `bot_chat` | `sidelinja/resonering` | Bot-svar i chat (Claude-agent og andre bots) |
|
||||
| `bot_triage` | `sidelinja/rutine` | Triagering og klassifisering av innkommende meldinger |
|
||||
| `agent_respond` | `sidelinja/resonering` | Claude chat-agent svar (legacy — bruk `bot_chat`) |
|
||||
|
||||
**Analyse & klassifisering:**
|
||||
|
||||
| Kontekst | Standard alias | Begrunnelse |
|
||||
|---|---|---|
|
||||
| `suggest_edges` | `sidelinja/rutine` | AI-foreslåtte topics og mentions ved ny node |
|
||||
| `summarize` | `sidelinja/rutine` | Oppsummering av kommunikasjonsnoder og innhold |
|
||||
| `classify` | `sidelinja/rutine` | Klassifisering av innhold (node_kind, tags, prioritet) |
|
||||
|
||||
**Prosessering:**
|
||||
|
||||
| Kontekst | Standard alias | Begrunnelse |
|
||||
|---|---|---|
|
||||
| `ai_text_process` | `sidelinja/rutine` | Tekstvasking, høyt volum |
|
||||
| `whisper_postprocess` | `sidelinja/rutine` | Transkripsjonsvasking, høyt volum |
|
||||
| `research_clip` | `sidelinja/rutine` | Research-oppsummering, høyt volum |
|
||||
| `suggest_edges` | `sidelinja/rutine` | AI-foreslåtte topics og mentions, automatisk ved ny node |
|
||||
| `simple_prompt` | `sidelinja/rutine` | Standard LLM-kall via synops-ai prompt |
|
||||
| `live_factoid_eval` | `sidelinja/resonering` | Krever presis vurdering under tidspress |
|
||||
|
||||
Modellalias lagres som felt på jobben i PG — kan overstyres manuelt per jobb ved behov.
|
||||
**Teknisk:**
|
||||
Maskinrommet slår opp modellalias via `ai_admin::resolve_routing_or_default(db, kontekst)`.
|
||||
Resultatet sendes som miljøvariabel til CLI-verktøy (f.eks. `AI_EDGES_MODEL`, `AI_SUMMARY_MODEL`).
|
||||
CLI-verktøy som `synops-ai` gjør oppslag direkte i tabellen via `--job-type`-flagget.
|
||||
|
||||
### 3.5 Admin-panel (`/admin/ai`)
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,55 @@
|
|||
let newRoutingAlias = $state('');
|
||||
let newRoutingDesc = $state('');
|
||||
|
||||
// Kjente AI-kontekster med kategorier og beskrivelser
|
||||
const KNOWN_CONTEXTS: { category: string; contexts: { job_type: string; description: string }[] }[] = [
|
||||
{
|
||||
category: 'Orkestrering',
|
||||
contexts: [
|
||||
{ job_type: 'orchestration_script', description: 'LLM-kall i orkestreringsskript (SPØR/TRANSFORMER-steg)' },
|
||||
{ job_type: 'orchestration_dream', description: 'Kreativ/utforskende orkestrering — drømmemodus' }
|
||||
]
|
||||
},
|
||||
{
|
||||
category: 'Bot & chat',
|
||||
contexts: [
|
||||
{ job_type: 'bot_chat', description: 'Bot-svar i chat (Claude-agent og andre bots)' },
|
||||
{ job_type: 'bot_triage', description: 'Triagering og klassifisering av innkommende meldinger' },
|
||||
{ job_type: 'agent_respond', description: 'Claude chat-agent svar (legacy)' }
|
||||
]
|
||||
},
|
||||
{
|
||||
category: 'Analyse & klassifisering',
|
||||
contexts: [
|
||||
{ job_type: 'suggest_edges', description: 'AI-foreslåtte topics og mentions ved ny node' },
|
||||
{ job_type: 'summarize', description: 'Oppsummering av kommunikasjonsnoder og innhold' },
|
||||
{ job_type: 'classify', description: 'Klassifisering av innhold (node_kind, tags, prioritet)' }
|
||||
]
|
||||
},
|
||||
{
|
||||
category: 'Prosessering',
|
||||
contexts: [
|
||||
{ job_type: 'ai_text_process', description: 'Tekstvasking og behandling, høyt volum' },
|
||||
{ job_type: 'whisper_postprocess', description: 'Transkripsjonsvasking etter Whisper' },
|
||||
{ job_type: 'research_clip', description: 'Research-oppsummering' },
|
||||
{ job_type: 'simple_prompt', description: 'Standard LLM-kall via synops-ai prompt' },
|
||||
{ job_type: 'live_factoid_eval', description: 'Faktoid-vurdering under live sending' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Alle kjente job_types som flat liste
|
||||
const allKnownJobTypes = KNOWN_CONTEXTS.flatMap((c) => c.contexts.map((ctx) => ctx.job_type));
|
||||
|
||||
function routingForContext(jobType: string): AiJobRouting | undefined {
|
||||
return data?.routing.find((r) => r.job_type === jobType);
|
||||
}
|
||||
|
||||
function unknownRoutingRules(): AiJobRouting[] {
|
||||
if (!data) return [];
|
||||
return data.routing.filter((r) => !allKnownJobTypes.includes(r.job_type));
|
||||
}
|
||||
|
||||
// Poll hvert 10. sekund (AI-config endres sjelden)
|
||||
$effect(() => {
|
||||
if (!accessToken) return;
|
||||
|
|
@ -483,20 +532,85 @@
|
|||
|
||||
<!-- === TAB: Ruting === -->
|
||||
{:else if activeTab === 'routing'}
|
||||
<section class="rounded-lg border border-gray-200 bg-white shadow-sm">
|
||||
<div class="border-b border-gray-100 px-5 py-3">
|
||||
<h2 class="text-sm font-semibold text-gray-800">Jobbtype → Modellalias</h2>
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-5 py-3 shadow-sm">
|
||||
<h2 class="text-sm font-semibold text-gray-800">AI-kontekster → Modellalias</h2>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Hvilken modellalias brukes for hvilken jobbtype i jobbkøen.
|
||||
Hvilken modellalias brukes i hvilken kontekst. Endringer trer i kraft umiddelbart uten redeploy.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#each KNOWN_CONTEXTS as group}
|
||||
<section class="rounded-lg border border-gray-200 bg-white shadow-sm">
|
||||
<div class="border-b border-gray-100 px-5 py-2.5">
|
||||
<h3 class="text-xs font-semibold uppercase tracking-wide text-gray-500">
|
||||
{group.category}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-100">
|
||||
{#each data.routing as routing}
|
||||
{#each group.contexts as ctx}
|
||||
{@const routing = routingForContext(ctx.job_type)}
|
||||
<div class="flex items-center gap-3 px-5 py-3">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-mono text-sm text-gray-800">{ctx.job_type}</span>
|
||||
{#if !routing}
|
||||
<span class="rounded bg-gray-100 px-1.5 py-0.5 text-[10px] text-gray-400">
|
||||
ikke konfigurert
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="mt-0.5 text-xs text-gray-400">{ctx.description}</p>
|
||||
</div>
|
||||
<select
|
||||
class="rounded border border-gray-300 px-2 py-1 text-sm {!routing ? 'text-gray-400' : ''}"
|
||||
value={routing?.alias ?? ''}
|
||||
onchange={(e) => {
|
||||
const val = (e.target as HTMLSelectElement).value;
|
||||
if (val) {
|
||||
const r = routing ?? { job_type: ctx.job_type, alias: val, description: ctx.description };
|
||||
handleUpdateRouting(r, val);
|
||||
}
|
||||
}}
|
||||
disabled={actionLoading === `routing-${ctx.job_type}`}
|
||||
>
|
||||
{#if !routing}
|
||||
<option value="">Velg alias...</option>
|
||||
{/if}
|
||||
{#each data.aliases as alias}
|
||||
<option value={alias.alias}>{alias.alias}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if routing}
|
||||
<button
|
||||
class="rounded px-2 py-1 text-xs text-red-500 hover:bg-red-50"
|
||||
onclick={() => handleDeleteRouting(ctx.job_type)}
|
||||
disabled={actionLoading === `delete-routing-${ctx.job_type}`}
|
||||
>
|
||||
Fjern
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/each}
|
||||
|
||||
<!-- Egendefinerte regler (ikke i kjente kontekster) -->
|
||||
{#if unknownRoutingRules().length > 0}
|
||||
<section class="rounded-lg border border-gray-200 bg-white shadow-sm">
|
||||
<div class="border-b border-gray-100 px-5 py-2.5">
|
||||
<h3 class="text-xs font-semibold uppercase tracking-wide text-gray-500">
|
||||
Egendefinerte
|
||||
</h3>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-100">
|
||||
{#each unknownRoutingRules() as routing}
|
||||
<div class="flex items-center gap-3 px-5 py-3">
|
||||
<div class="flex-1">
|
||||
<span class="font-mono text-sm text-gray-800">{routing.job_type}</span>
|
||||
{#if routing.description}
|
||||
<span class="ml-2 text-xs text-gray-400">{routing.description}</span>
|
||||
<p class="mt-0.5 text-xs text-gray-400">{routing.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<select
|
||||
|
|
@ -514,23 +628,22 @@
|
|||
onclick={() => handleDeleteRouting(routing.job_type)}
|
||||
disabled={actionLoading === `delete-routing-${routing.job_type}`}
|
||||
>
|
||||
Slett
|
||||
Fjern
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if data.routing.length === 0}
|
||||
<p class="px-5 py-4 text-sm text-gray-400">Ingen ruting-regler konfigurert.</p>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<!-- Ny ruting -->
|
||||
<!-- Ny egendefinert ruting -->
|
||||
{#if showNewRouting}
|
||||
<div class="border-t border-blue-200 bg-blue-50 px-5 py-3">
|
||||
<section class="rounded-lg border border-blue-200 bg-blue-50 p-5 shadow-sm">
|
||||
<h3 class="mb-3 text-sm font-semibold text-gray-800">Ny egendefinert rutingregel</h3>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
bind:value={newRoutingJobType}
|
||||
placeholder="Jobbtype (f.eks. ai_text_process)"
|
||||
placeholder="Jobbtype (f.eks. min_jobb)"
|
||||
class="flex-1 rounded border border-gray-300 px-2 py-1 text-sm"
|
||||
/>
|
||||
<select
|
||||
|
|
@ -565,18 +678,16 @@
|
|||
Avbryt
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{:else}
|
||||
<div class="border-t border-gray-100 px-5 py-3">
|
||||
<button
|
||||
class="text-xs text-blue-600 hover:text-blue-800"
|
||||
class="rounded-lg border border-dashed border-gray-300 px-4 py-2 text-sm text-gray-500 hover:border-gray-400 hover:text-gray-700"
|
||||
onclick={() => (showNewRouting = true)}
|
||||
>
|
||||
+ Ny rutingregel
|
||||
+ Ny egendefinert rutingregel
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- === TAB: Forbruk === -->
|
||||
{:else if activeTab === 'usage'}
|
||||
|
|
|
|||
|
|
@ -86,6 +86,40 @@ fn internal_error(msg: &str) -> (StatusCode, Json<ErrorResponse>) {
|
|||
(StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: msg.to_string() }))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Modelloppslag fra ai_job_routing — brukes av jobbkø-dispatchers
|
||||
// =============================================================================
|
||||
|
||||
/// Slå opp modellalias fra ai_job_routing for en gitt jobbtype/kontekst.
|
||||
/// Returnerer None hvis ingen regel er konfigurert.
|
||||
pub async fn resolve_routing(db: &PgPool, job_type: &str) -> Result<Option<String>, sqlx::Error> {
|
||||
let row: Option<(String,)> =
|
||||
sqlx::query_as("SELECT alias FROM ai_job_routing WHERE job_type = $1")
|
||||
.bind(job_type)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
Ok(row.map(|(alias,)| alias))
|
||||
}
|
||||
|
||||
/// Slå opp modellalias med fallback til standard-alias.
|
||||
/// Brukes av dispatchers som trenger en modell uansett.
|
||||
pub async fn resolve_routing_or_default(db: &PgPool, job_type: &str) -> String {
|
||||
match resolve_routing(db, job_type).await {
|
||||
Ok(Some(alias)) => {
|
||||
tracing::debug!(job_type, alias = %alias, "Modell fra ai_job_routing");
|
||||
alias
|
||||
}
|
||||
Ok(None) => {
|
||||
tracing::warn!(job_type, "Ingen rutingregel — bruker sidelinja/rutine");
|
||||
"sidelinja/rutine".to_string()
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(job_type, error = %e, "Feil ved oppslag i ai_job_routing — bruker fallback");
|
||||
"sidelinja/rutine".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GET /admin/ai — oversikt
|
||||
// =============================================================================
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::ai_admin;
|
||||
use crate::cli_dispatch;
|
||||
use crate::jobs::JobRow;
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ fn suggest_edges_bin() -> String {
|
|||
/// LLM-kall, topic-opprettelse, edge-skriving, ressurslogging.
|
||||
pub async fn handle_suggest_edges(
|
||||
job: &JobRow,
|
||||
_db: &sqlx::PgPool,
|
||||
db: &sqlx::PgPool,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let node_id: Uuid = job
|
||||
.payload
|
||||
|
|
@ -55,7 +56,10 @@ pub async fn handle_suggest_edges(
|
|||
cli_dispatch::set_database_url(&mut cmd)?;
|
||||
cli_dispatch::forward_env(&mut cmd, "AI_GATEWAY_URL");
|
||||
cli_dispatch::forward_env(&mut cmd, "LITELLM_MASTER_KEY");
|
||||
cli_dispatch::forward_env(&mut cmd, "AI_EDGES_MODEL");
|
||||
|
||||
// Modellalias fra ai_job_routing — admin kan endre uten redeploy
|
||||
let model_alias = ai_admin::resolve_routing_or_default(db, "suggest_edges").await;
|
||||
cmd.env("AI_EDGES_MODEL", &model_alias);
|
||||
|
||||
tracing::info!(
|
||||
node_id = %node_id,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::ai_admin;
|
||||
use crate::cli_dispatch;
|
||||
use crate::jobs::JobRow;
|
||||
|
||||
|
|
@ -28,7 +29,7 @@ fn summarize_bin() -> String {
|
|||
/// - requested_by: UUID — brukeren som utløste oppsummeringen
|
||||
pub async fn handle_summarize_communication(
|
||||
job: &JobRow,
|
||||
_db: &sqlx::PgPool,
|
||||
db: &sqlx::PgPool,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let communication_id: Uuid = job
|
||||
.payload
|
||||
|
|
@ -58,7 +59,10 @@ pub async fn handle_summarize_communication(
|
|||
cli_dispatch::set_database_url(&mut cmd)?;
|
||||
cli_dispatch::forward_env(&mut cmd, "AI_GATEWAY_URL");
|
||||
cli_dispatch::forward_env(&mut cmd, "LITELLM_MASTER_KEY");
|
||||
cli_dispatch::forward_env(&mut cmd, "AI_SUMMARY_MODEL");
|
||||
|
||||
// Modellalias fra ai_job_routing — admin kan endre uten redeploy
|
||||
let model_alias = ai_admin::resolve_routing_or_default(db, "summarize").await;
|
||||
cmd.env("AI_SUMMARY_MODEL", &model_alias);
|
||||
|
||||
tracing::info!(
|
||||
communication_id = %communication_id,
|
||||
|
|
|
|||
16
migrations/028_ai_routing_contexts.sql
Normal file
16
migrations/028_ai_routing_contexts.sql
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
-- 028_ai_routing_contexts.sql — Utvid ai_job_routing med kontekster for oppgave 28.2
|
||||
--
|
||||
-- Legger til de syv AI-kontekstene som admin kan konfigurere i /admin/ai.
|
||||
-- Eksisterende rader beholdes (ON CONFLICT DO NOTHING).
|
||||
--
|
||||
-- Ref: docs/infra/ai_gateway.md §3.4
|
||||
|
||||
INSERT INTO ai_job_routing (job_type, alias, description) VALUES
|
||||
('orchestration_script', 'sidelinja/rutine', 'LLM-kall i orkestreringsskript (SPØR/TRANSFORMER-steg)'),
|
||||
('orchestration_dream', 'sidelinja/resonering', 'Kreativ/utforskende orkestrering — drømmemodus'),
|
||||
('bot_chat', 'sidelinja/resonering', 'Bot-svar i chat (Claude-agent og andre bots)'),
|
||||
('bot_triage', 'sidelinja/rutine', 'Triagering og klassifisering av innkommende meldinger'),
|
||||
('summarize', 'sidelinja/rutine', 'Oppsummering av kommunikasjonsnoder og innhold'),
|
||||
('suggest_edges', 'sidelinja/rutine', 'AI-foreslåtte topics og mentions ved ny node'),
|
||||
('classify', 'sidelinja/rutine', 'Klassifisering av innhold (node_kind, tags, prioritet)')
|
||||
ON CONFLICT (job_type) DO NOTHING;
|
||||
3
tasks.md
3
tasks.md
|
|
@ -373,8 +373,7 @@ modell som brukes til hva.
|
|||
### synops-ai: lettvekts LLM-kall
|
||||
|
||||
- [x] 28.1 `synops-ai` CLI: direkte LLM-kall via LiteLLM. Input: `--prompt <tekst> [--model <alias>] [--system <systemprompt>]`. Output: tekst til stdout. Ingen fillesing, ingen verktøy, bare prompt inn/ut. Bruker `ai_job_routing`-tabellen for å bestemme modell hvis `--model` ikke er satt. Logger i `ai_usage_log`.
|
||||
- [~] 28.2 AI-rutingskontroll i admin: utvid admin-UI (fase 15.4) med konfigurasjon av hvilken modell som brukes per kontekst. Tabellen `ai_job_routing` mapper `(job_type, context)` → `model_alias`. Kontekster: `orchestration_script`, `orchestration_dream`, `bot_chat`, `bot_triage`, `summarize`, `suggest_edges`, `classify`. Admin kan endre uten redeploy.
|
||||
> Påbegynt: 2026-03-18T19:55
|
||||
- [x] 28.2 AI-rutingskontroll i admin: utvid admin-UI (fase 15.4) med konfigurasjon av hvilken modell som brukes per kontekst. Tabellen `ai_job_routing` mapper `(job_type, context)` → `model_alias`. Kontekster: `orchestration_script`, `orchestration_dream`, `bot_chat`, `bot_triage`, `summarize`, `suggest_edges`, `classify`. Admin kan endre uten redeploy.
|
||||
- [ ] 28.3 Kostnadstak per bruker/samling: `ai_budget`-felt i metadata for brukere og samlinger. `synops-ai` sjekker budsjett mot `ai_usage_log` aggregat før kall. Ved overskridelse: returner feilmelding, opprett work_item.
|
||||
|
||||
### Øvrige manglende verktøy
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue