Fullfører oppgave 18.3: Direction-logikk for AI-prosessering
Implementerer de to retningene for AI-verktøyet:
- tool_to_node ("Penselen"): Lagrer original content som revisjon i
ny node_revisions-tabell, deretter oppdaterer noden med AI-output
i både STDB (sanntid) og PG (persistering).
- node_to_tool ("Kverna"): Oppretter ny node med AI-output, med
derived_from-edge tilbake til kildenoden og processed_by-edge
til AI-preseten. Full sporbarhet i grafen.
Ny PG-tabell: node_revisions (node_id, content, title, metadata,
revision_type, created_by, ai_preset_id, job_id).
Ref: docs/features/ai_verktoy.md § 2.2, § 6.1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9855488d12
commit
776bc895c1
3 changed files with 242 additions and 21 deletions
|
|
@ -1,4 +1,5 @@
|
|||
// AI-prosessering — hent kilde-content + preset-prompt, kall AI Gateway.
|
||||
// AI-prosessering — hent kilde-content + preset-prompt, kall AI Gateway,
|
||||
// og utfør direction-spesifikk logikk.
|
||||
//
|
||||
// Jobbtype: "ai_process"
|
||||
// Payload: {
|
||||
|
|
@ -14,7 +15,9 @@
|
|||
// 3. Map modellprofil → LiteLLM-alias (flash → sidelinja/rutine, standard → sidelinja/resonering)
|
||||
// 4. Send til AI Gateway (LiteLLM)
|
||||
// 5. Logg forbruk i ai_usage_log
|
||||
// 6. Returner AI-output (direction-logikk implementeres i oppgave 18.3)
|
||||
// 6. Direction-logikk:
|
||||
// - tool_to_node: lagre original som revisjon i node_revisions, oppdater node content
|
||||
// - node_to_tool: opprett ny node med AI-output, opprett derived_from + processed_by edges
|
||||
//
|
||||
// Ref: docs/features/ai_verktoy.md, docs/infra/ai_gateway.md
|
||||
|
||||
|
|
@ -24,14 +27,15 @@ use uuid::Uuid;
|
|||
|
||||
use crate::jobs::JobRow;
|
||||
use crate::resource_usage;
|
||||
use crate::stdb::StdbClient;
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct SourceNodeRow {
|
||||
content: Option<String>,
|
||||
#[allow(dead_code)] // Brukes i oppgave 18.3 (direction-logikk)
|
||||
title: Option<String>,
|
||||
#[allow(dead_code)] // Brukes i oppgave 18.3 (direction-logikk)
|
||||
node_kind: String,
|
||||
visibility: String,
|
||||
metadata: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
|
|
@ -96,6 +100,7 @@ fn model_profile_to_alias(profile: &str) -> &'static str {
|
|||
pub async fn handle_ai_process(
|
||||
job: &JobRow,
|
||||
db: &PgPool,
|
||||
stdb: &StdbClient,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let source_node_id: Uuid = job
|
||||
.payload
|
||||
|
|
@ -124,9 +129,9 @@ pub async fn handle_ai_process(
|
|||
.and_then(|s| s.parse().ok())
|
||||
.ok_or("Mangler requested_by i payload")?;
|
||||
|
||||
// 1. Hent kilde-node
|
||||
// 1. Hent kilde-node (inkl. visibility og metadata for direction-logikk)
|
||||
let source = sqlx::query_as::<_, SourceNodeRow>(
|
||||
"SELECT content, title, node_kind FROM nodes WHERE id = $1",
|
||||
"SELECT content, title, node_kind, visibility::text AS visibility, metadata FROM nodes WHERE id = $1",
|
||||
)
|
||||
.bind(source_node_id)
|
||||
.fetch_optional(db)
|
||||
|
|
@ -136,8 +141,10 @@ pub async fn handle_ai_process(
|
|||
|
||||
let source_content = source
|
||||
.content
|
||||
.as_ref()
|
||||
.filter(|c| !c.is_empty())
|
||||
.ok_or("Kilde-noden har ikke innhold å behandle")?;
|
||||
.ok_or("Kilde-noden har ikke innhold å behandle")?
|
||||
.clone();
|
||||
|
||||
// 2. Hent AI-preset
|
||||
let preset = sqlx::query_as::<_, PresetRow>(
|
||||
|
|
@ -240,18 +247,233 @@ pub async fn handle_ai_process(
|
|||
tracing::warn!(error = %e, "Kunne ikke logge AI-ressursforbruk");
|
||||
}
|
||||
|
||||
// 6. Returner resultat
|
||||
// Direction-logikk (opprett ny node / oppdater eksisterende) implementeres i oppgave 18.3
|
||||
Ok(serde_json::json!({
|
||||
"status": "completed",
|
||||
// 6. Direction-logikk
|
||||
match direction {
|
||||
"tool_to_node" => {
|
||||
handle_tool_to_node(
|
||||
db, stdb, job, source_node_id, ai_preset_id, requested_by,
|
||||
&source, &source_content, &ai_output,
|
||||
).await?;
|
||||
|
||||
tracing::info!(
|
||||
source_node_id = %source_node_id,
|
||||
"tool_to_node: original lagret som revisjon, node oppdatert med AI-output"
|
||||
);
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"status": "completed",
|
||||
"source_node_id": source_node_id.to_string(),
|
||||
"ai_preset_id": ai_preset_id.to_string(),
|
||||
"direction": "tool_to_node",
|
||||
"ai_output": ai_output,
|
||||
"tokens_in": tokens_in,
|
||||
"tokens_out": tokens_out,
|
||||
"total_tokens": total_tokens
|
||||
}))
|
||||
}
|
||||
"node_to_tool" => {
|
||||
let new_node_id = handle_node_to_tool(
|
||||
db, stdb, source_node_id, ai_preset_id, requested_by,
|
||||
&source, &ai_output, preset.title.as_deref(),
|
||||
).await?;
|
||||
|
||||
tracing::info!(
|
||||
source_node_id = %source_node_id,
|
||||
new_node_id = %new_node_id,
|
||||
"node_to_tool: ny node opprettet med derived_from + processed_by edges"
|
||||
);
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"status": "completed",
|
||||
"source_node_id": source_node_id.to_string(),
|
||||
"new_node_id": new_node_id.to_string(),
|
||||
"ai_preset_id": ai_preset_id.to_string(),
|
||||
"direction": "node_to_tool",
|
||||
"ai_output": ai_output,
|
||||
"tokens_in": tokens_in,
|
||||
"tokens_out": tokens_out,
|
||||
"total_tokens": total_tokens
|
||||
}))
|
||||
}
|
||||
other => Err(format!("Ugyldig direction: {other}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// tool_to_node: "Penselen" — AI-verktøyet brukes PÅ noden.
|
||||
/// Lagrer original content som revisjon, oppdaterer noden med AI-output.
|
||||
/// Ref: docs/features/ai_verktoy.md § 2.2
|
||||
async fn handle_tool_to_node(
|
||||
db: &PgPool,
|
||||
stdb: &StdbClient,
|
||||
job: &JobRow,
|
||||
source_node_id: Uuid,
|
||||
ai_preset_id: Uuid,
|
||||
requested_by: Uuid,
|
||||
source: &SourceNodeRow,
|
||||
original_content: &str,
|
||||
ai_output: &str,
|
||||
) -> Result<(), String> {
|
||||
// 1. Lagre originalt innhold som revisjon
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO node_revisions (node_id, content, title, metadata, revision_type, created_by, ai_preset_id, job_id)
|
||||
VALUES ($1, $2, $3, $4, 'ai_edit', $5, $6, $7)
|
||||
"#,
|
||||
)
|
||||
.bind(source_node_id)
|
||||
.bind(original_content)
|
||||
.bind(source.title.as_deref())
|
||||
.bind(&source.metadata)
|
||||
.bind(requested_by)
|
||||
.bind(ai_preset_id)
|
||||
.bind(job.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| format!("Kunne ikke lagre revisjon: {e}"))?;
|
||||
|
||||
// 2. Oppdater node content i STDB (sanntid)
|
||||
let metadata_str = source.metadata.to_string();
|
||||
stdb.update_node(
|
||||
&source_node_id.to_string(),
|
||||
&source.node_kind,
|
||||
source.title.as_deref().unwrap_or(""),
|
||||
ai_output,
|
||||
&source.visibility,
|
||||
&metadata_str,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("STDB update_node feilet: {e}"))?;
|
||||
|
||||
// 3. Oppdater node content i PG (persistering)
|
||||
sqlx::query(
|
||||
"UPDATE nodes SET content = $2 WHERE id = $1",
|
||||
)
|
||||
.bind(source_node_id)
|
||||
.bind(ai_output)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| format!("PG update node content feilet: {e}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// node_to_tool: "Kverna" — noden sendes GJENNOM verktøyet.
|
||||
/// Oppretter ny node med AI-output, med derived_from-edge til kilde
|
||||
/// og processed_by-edge til AI-preset.
|
||||
/// Ref: docs/features/ai_verktoy.md § 2.2
|
||||
async fn handle_node_to_tool(
|
||||
db: &PgPool,
|
||||
stdb: &StdbClient,
|
||||
source_node_id: Uuid,
|
||||
ai_preset_id: Uuid,
|
||||
requested_by: Uuid,
|
||||
source: &SourceNodeRow,
|
||||
ai_output: &str,
|
||||
preset_title: Option<&str>,
|
||||
) -> Result<Uuid, String> {
|
||||
let new_node_id = Uuid::now_v7();
|
||||
let new_title = format!(
|
||||
"{} → {}",
|
||||
source.title.as_deref().unwrap_or("Uten tittel"),
|
||||
preset_title.unwrap_or("AI"),
|
||||
);
|
||||
let new_metadata = serde_json::json!({
|
||||
"ai_generated": true,
|
||||
"source_node_id": source_node_id.to_string(),
|
||||
"ai_preset_id": ai_preset_id.to_string(),
|
||||
"direction": direction,
|
||||
"ai_output": ai_output,
|
||||
"tokens_in": tokens_in,
|
||||
"tokens_out": tokens_out,
|
||||
"total_tokens": total_tokens
|
||||
}))
|
||||
"ai_preset_id": ai_preset_id.to_string()
|
||||
});
|
||||
let new_metadata_str = new_metadata.to_string();
|
||||
let empty_meta = serde_json::json!({}).to_string();
|
||||
|
||||
// 1. Opprett ny node i STDB (sanntid)
|
||||
stdb.create_node(
|
||||
&new_node_id.to_string(),
|
||||
"content",
|
||||
&new_title,
|
||||
ai_output,
|
||||
&source.visibility,
|
||||
&new_metadata_str,
|
||||
&requested_by.to_string(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("STDB create_node feilet: {e}"))?;
|
||||
|
||||
// 2. Opprett ny node i PG (persistering)
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO nodes (id, node_kind, title, content, visibility, metadata, created_by)
|
||||
VALUES ($1, 'content', $2, $3, $4::visibility, $5, $6)
|
||||
"#,
|
||||
)
|
||||
.bind(new_node_id)
|
||||
.bind(&new_title)
|
||||
.bind(ai_output)
|
||||
.bind(&source.visibility)
|
||||
.bind(&new_metadata)
|
||||
.bind(requested_by)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| format!("PG insert ny node feilet: {e}"))?;
|
||||
|
||||
// 3. Opprett derived_from-edge: ny node → kilde-node
|
||||
// Sporbarhet: "denne noden er avledet fra kilden"
|
||||
let derived_edge_id = Uuid::now_v7();
|
||||
stdb.create_edge(
|
||||
&derived_edge_id.to_string(),
|
||||
&new_node_id.to_string(),
|
||||
&source_node_id.to_string(),
|
||||
"derived_from",
|
||||
&empty_meta,
|
||||
false,
|
||||
&requested_by.to_string(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("STDB create_edge (derived_from) feilet: {e}"))?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO edges (id, source_id, target_id, edge_type, metadata, system, created_by)
|
||||
VALUES ($1, $2, $3, 'derived_from', '{}', false, $4)
|
||||
"#,
|
||||
)
|
||||
.bind(derived_edge_id)
|
||||
.bind(new_node_id)
|
||||
.bind(source_node_id)
|
||||
.bind(requested_by)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| format!("PG insert derived_from-edge feilet: {e}"))?;
|
||||
|
||||
// 4. Opprett processed_by-edge: ny node → AI-preset
|
||||
// Sporbarhet: "denne noden ble prosessert av dette AI-verktøyet"
|
||||
let processed_edge_id = Uuid::now_v7();
|
||||
stdb.create_edge(
|
||||
&processed_edge_id.to_string(),
|
||||
&new_node_id.to_string(),
|
||||
&ai_preset_id.to_string(),
|
||||
"processed_by",
|
||||
&empty_meta,
|
||||
false,
|
||||
&requested_by.to_string(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("STDB create_edge (processed_by) feilet: {e}"))?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO edges (id, source_id, target_id, edge_type, metadata, system, created_by)
|
||||
VALUES ($1, $2, $3, 'processed_by', '{}', false, $4)
|
||||
"#,
|
||||
)
|
||||
.bind(processed_edge_id)
|
||||
.bind(new_node_id)
|
||||
.bind(ai_preset_id)
|
||||
.bind(requested_by)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| format!("PG insert processed_by-edge feilet: {e}"))?;
|
||||
|
||||
Ok(new_node_id)
|
||||
}
|
||||
|
||||
/// Kall AI Gateway (LiteLLM) for tekstbehandling.
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ async fn dispatch(
|
|||
audio::handle_audio_process_job(job, db, stdb, cas).await
|
||||
}
|
||||
"ai_process" => {
|
||||
ai_process::handle_ai_process(job, db).await
|
||||
ai_process::handle_ai_process(job, db, stdb).await
|
||||
}
|
||||
"render_article" => {
|
||||
handle_render_article(job, db, cas).await
|
||||
|
|
|
|||
3
tasks.md
3
tasks.md
|
|
@ -203,8 +203,7 @@ Ref: `docs/features/ai_verktoy.md`, `docs/retninger/arbeidsflaten.md`
|
|||
|
||||
- [x] 18.1 AI-preset node-type: `node_kind: 'ai_preset'` med metadata (prompt, model_profile, category, icon, color). Maskinrommet validerer ved opprettelse. Seed standardprompter (rens tekst, korrektør, oppsummering, oversett, skriv om, trekk ut fakta, forenkle, endre tone).
|
||||
- [x] 18.2 AI-prosessering endepunkt: `POST /intentions/ai_process` med source_node_id, ai_preset_id, direction (node_to_tool / tool_to_node). Maskinrommet henter kilde-content og preset-prompt, mapper modellprofil → LiteLLM-alias, sender til AI Gateway. Logg forbruk i ai_usage_log.
|
||||
- [~] 18.3 Direction-logikk: `tool_to_node` → lagre original som revisjon, oppdater node content. `node_to_tool` → opprett ny node med AI-output, opprett `derived_from`-edge til kilde + `processed_by`-edge til AI-preset.
|
||||
> Påbegynt: 2026-03-18T06:25
|
||||
- [x] 18.3 Direction-logikk: `tool_to_node` → lagre original som revisjon, oppdater node content. `node_to_tool` → opprett ny node med AI-output, opprett `derived_from`-edge til kilde + `processed_by`-edge til AI-preset.
|
||||
- [ ] 18.4 AI-verktøy panel (frontend): Svelte-komponent for arbeidsflaten. Prompt-velger med standardprompter, fritekst-felt for egendefinert prompt, modell-indikator (readonly). Drag-and-drop mottak for tekstnoder.
|
||||
- [ ] 18.5 Drag-and-drop integrasjon: node → verktøy (ny node), verktøy → node (in-place revisjon). Drop-sone feedback med verktøyets farge. Inkompatibilitet for lyd/bilde-noder med forklaring.
|
||||
- [ ] 18.6 Egendefinerte presets: brukere kan opprette egne AI-preset-noder med custom prompt. Dele via edges til samling/team. Modellprofil satt av admin.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue