Fullfører oppgave 18.2: AI-prosessering endepunkt
POST /intentions/ai_process med source_node_id, ai_preset_id og direction (node_to_tool / tool_to_node). Endepunktet validerer input, sjekker at kilde-node og AI-preset finnes, verifiserer skrivetilgang for tool_to_node-retning, og legger en ai_process-jobb i køen. Jobb-handleren (ai_process.rs) henter kilde-content og preset-prompt, mapper modellprofil → LiteLLM-alias (flash → sidelinja/rutine, standard → sidelinja/resonering), kaller AI Gateway, og logger forbruk i både ai_usage_log og resource_usage_log. Direction-logikk (opprett ny node vs. oppdater eksisterende) implementeres i oppgave 18.3. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7224cf9897
commit
bca0ff1deb
6 changed files with 450 additions and 3 deletions
|
|
@ -225,7 +225,7 @@ Flere AI-verktøy i serie: dra output fra "Oversett" videre til
|
||||||
### Fase A: Grunnleggende verktøy-panel
|
### Fase A: Grunnleggende verktøy-panel
|
||||||
- [x] AI-preset node-type (`node_kind: 'ai_preset'`) + metadata-skjema
|
- [x] AI-preset node-type (`node_kind: 'ai_preset'`) + metadata-skjema
|
||||||
- [x] Standard-presets som seed-data (rens tekst, korrektør, oppsummering osv.)
|
- [x] Standard-presets som seed-data (rens tekst, korrektør, oppsummering osv.)
|
||||||
- [ ] `POST /intentions/ai_process` endepunkt i maskinrommet
|
- [x] `POST /intentions/ai_process` endepunkt i maskinrommet
|
||||||
- [ ] Verktøy-panel UI med prompt-velger og modell-indikator
|
- [ ] Verktøy-panel UI med prompt-velger og modell-indikator
|
||||||
- [ ] Jobbkø-integrasjon med AI Gateway
|
- [ ] Jobbkø-integrasjon med AI Gateway
|
||||||
|
|
||||||
|
|
|
||||||
314
maskinrommet/src/ai_process.rs
Normal file
314
maskinrommet/src/ai_process.rs
Normal file
|
|
@ -0,0 +1,314 @@
|
||||||
|
// AI-prosessering — hent kilde-content + preset-prompt, kall AI Gateway.
|
||||||
|
//
|
||||||
|
// Jobbtype: "ai_process"
|
||||||
|
// Payload: {
|
||||||
|
// "source_node_id": "<uuid>",
|
||||||
|
// "ai_preset_id": "<uuid>",
|
||||||
|
// "direction": "node_to_tool" | "tool_to_node",
|
||||||
|
// "requested_by": "<uuid>"
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Flyten:
|
||||||
|
// 1. Hent kilde-node content fra PG
|
||||||
|
// 2. Hent AI-preset prompt + modellprofil fra PG
|
||||||
|
// 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)
|
||||||
|
//
|
||||||
|
// Ref: docs/features/ai_verktoy.md, docs/infra/ai_gateway.md
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::jobs::JobRow;
|
||||||
|
use crate::resource_usage;
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
struct PresetRow {
|
||||||
|
title: Option<String>,
|
||||||
|
metadata: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// OpenAI-kompatibel chat completion request.
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ChatRequest {
|
||||||
|
model: String,
|
||||||
|
messages: Vec<ChatMessage>,
|
||||||
|
temperature: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ChatMessage {
|
||||||
|
role: String,
|
||||||
|
content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// OpenAI-kompatibel chat completion response.
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ChatResponse {
|
||||||
|
choices: Vec<Choice>,
|
||||||
|
#[serde(default)]
|
||||||
|
usage: Option<UsageInfo>,
|
||||||
|
#[serde(default)]
|
||||||
|
model: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
struct UsageInfo {
|
||||||
|
#[serde(default)]
|
||||||
|
prompt_tokens: i64,
|
||||||
|
#[serde(default)]
|
||||||
|
completion_tokens: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Choice {
|
||||||
|
message: MessageContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct MessageContent {
|
||||||
|
content: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mapper modellprofil til LiteLLM-alias.
|
||||||
|
/// Ref: docs/features/ai_verktoy.md § 4, docs/infra/ai_gateway.md § 3.4
|
||||||
|
fn model_profile_to_alias(profile: &str) -> &'static str {
|
||||||
|
match profile {
|
||||||
|
"flash" => "sidelinja/rutine",
|
||||||
|
"standard" => "sidelinja/resonering",
|
||||||
|
_ => "sidelinja/rutine", // fallback til billigste
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Håndterer ai_process-jobb.
|
||||||
|
pub async fn handle_ai_process(
|
||||||
|
job: &JobRow,
|
||||||
|
db: &PgPool,
|
||||||
|
) -> Result<serde_json::Value, String> {
|
||||||
|
let source_node_id: Uuid = job
|
||||||
|
.payload
|
||||||
|
.get("source_node_id")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.ok_or("Mangler source_node_id i payload")?;
|
||||||
|
|
||||||
|
let ai_preset_id: Uuid = job
|
||||||
|
.payload
|
||||||
|
.get("ai_preset_id")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.ok_or("Mangler ai_preset_id i payload")?;
|
||||||
|
|
||||||
|
let direction = job
|
||||||
|
.payload
|
||||||
|
.get("direction")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or("Mangler direction i payload")?;
|
||||||
|
|
||||||
|
let requested_by: Uuid = job
|
||||||
|
.payload
|
||||||
|
.get("requested_by")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.ok_or("Mangler requested_by i payload")?;
|
||||||
|
|
||||||
|
// 1. Hent kilde-node
|
||||||
|
let source = sqlx::query_as::<_, SourceNodeRow>(
|
||||||
|
"SELECT content, title, node_kind FROM nodes WHERE id = $1",
|
||||||
|
)
|
||||||
|
.bind(source_node_id)
|
||||||
|
.fetch_optional(db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("PG-feil ved henting av kilde-node: {e}"))?
|
||||||
|
.ok_or("Kilde-node finnes ikke")?;
|
||||||
|
|
||||||
|
let source_content = source
|
||||||
|
.content
|
||||||
|
.filter(|c| !c.is_empty())
|
||||||
|
.ok_or("Kilde-noden har ikke innhold å behandle")?;
|
||||||
|
|
||||||
|
// 2. Hent AI-preset
|
||||||
|
let preset = sqlx::query_as::<_, PresetRow>(
|
||||||
|
"SELECT title, metadata FROM nodes WHERE id = $1 AND node_kind = 'ai_preset'",
|
||||||
|
)
|
||||||
|
.bind(ai_preset_id)
|
||||||
|
.fetch_optional(db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("PG-feil ved henting av AI-preset: {e}"))?
|
||||||
|
.ok_or("AI-preset finnes ikke")?;
|
||||||
|
|
||||||
|
let metadata = preset
|
||||||
|
.metadata
|
||||||
|
.ok_or("AI-preset mangler metadata")?;
|
||||||
|
|
||||||
|
let prompt = metadata
|
||||||
|
.get("prompt")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or("AI-preset mangler prompt i metadata")?;
|
||||||
|
|
||||||
|
let model_profile = metadata
|
||||||
|
.get("model_profile")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("flash");
|
||||||
|
|
||||||
|
// 3. Map modellprofil → LiteLLM-alias
|
||||||
|
let model_alias = model_profile_to_alias(model_profile);
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
source_node_id = %source_node_id,
|
||||||
|
ai_preset_id = %ai_preset_id,
|
||||||
|
direction = %direction,
|
||||||
|
model_alias = %model_alias,
|
||||||
|
preset_title = ?preset.title,
|
||||||
|
source_content_len = source_content.len(),
|
||||||
|
"Starter AI-prosessering"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Kall AI Gateway
|
||||||
|
let (ai_output, usage, actual_model) =
|
||||||
|
call_ai_gateway(model_alias, prompt, &source_content).await?;
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
source_node_id = %source_node_id,
|
||||||
|
output_len = ai_output.len(),
|
||||||
|
actual_model = ?actual_model,
|
||||||
|
"AI-prosessering fullført"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. Logg forbruk i ai_usage_log
|
||||||
|
let collection_id = resource_usage::find_collection_for_node(db, source_node_id).await;
|
||||||
|
let (tokens_in, tokens_out) = usage
|
||||||
|
.as_ref()
|
||||||
|
.map(|u| (u.prompt_tokens, u.completion_tokens))
|
||||||
|
.unwrap_or((0, 0));
|
||||||
|
let total_tokens = tokens_in + tokens_out;
|
||||||
|
|
||||||
|
// ai_usage_log — detaljert AI-forbrukslogg
|
||||||
|
if let Err(e) = sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT INTO ai_usage_log
|
||||||
|
(collection_node_id, job_id, model_alias, model_actual,
|
||||||
|
prompt_tokens, completion_tokens, total_tokens, job_type)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, 'ai_process')
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(collection_id)
|
||||||
|
.bind(job.id)
|
||||||
|
.bind(model_alias)
|
||||||
|
.bind(actual_model.as_deref())
|
||||||
|
.bind(tokens_in as i32)
|
||||||
|
.bind(tokens_out as i32)
|
||||||
|
.bind(total_tokens as i32)
|
||||||
|
.execute(db)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::warn!(error = %e, "Kunne ikke logge AI-forbruk i ai_usage_log");
|
||||||
|
}
|
||||||
|
|
||||||
|
// resource_usage_log — generell ressurslogging
|
||||||
|
if let Err(e) = resource_usage::log(
|
||||||
|
db,
|
||||||
|
source_node_id,
|
||||||
|
Some(requested_by),
|
||||||
|
collection_id,
|
||||||
|
"ai",
|
||||||
|
serde_json::json!({
|
||||||
|
"model_level": model_profile,
|
||||||
|
"model_id": actual_model.unwrap_or_else(|| "unknown".to_string()),
|
||||||
|
"model_alias": model_alias,
|
||||||
|
"tokens_in": tokens_in,
|
||||||
|
"tokens_out": tokens_out,
|
||||||
|
"job_type": "ai_process",
|
||||||
|
"preset_id": ai_preset_id.to_string(),
|
||||||
|
"direction": direction
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
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",
|
||||||
|
"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
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kall AI Gateway (LiteLLM) for tekstbehandling.
|
||||||
|
/// Returnerer (output_text, usage, actual_model_name).
|
||||||
|
async fn call_ai_gateway(
|
||||||
|
model_alias: &str,
|
||||||
|
system_prompt: &str,
|
||||||
|
user_content: &str,
|
||||||
|
) -> Result<(String, Option<UsageInfo>, Option<String>), String> {
|
||||||
|
let gateway_url = std::env::var("AI_GATEWAY_URL")
|
||||||
|
.unwrap_or_else(|_| "http://localhost:4000".to_string());
|
||||||
|
let api_key = std::env::var("LITELLM_MASTER_KEY").unwrap_or_default();
|
||||||
|
|
||||||
|
let request = ChatRequest {
|
||||||
|
model: model_alias.to_string(),
|
||||||
|
messages: vec![
|
||||||
|
ChatMessage {
|
||||||
|
role: "system".to_string(),
|
||||||
|
content: system_prompt.to_string(),
|
||||||
|
},
|
||||||
|
ChatMessage {
|
||||||
|
role: "user".to_string(),
|
||||||
|
content: user_content.to_string(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
temperature: 0.3,
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let url = format!("{gateway_url}/v1/chat/completions");
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.post(&url)
|
||||||
|
.header("Authorization", format!("Bearer {api_key}"))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.json(&request)
|
||||||
|
.timeout(std::time::Duration::from_secs(120))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("AI Gateway-kall feilet: {e}"))?;
|
||||||
|
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
let status = resp.status();
|
||||||
|
let body = resp.text().await.unwrap_or_default();
|
||||||
|
return Err(format!("AI Gateway returnerte {status}: {body}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let chat_resp: ChatResponse = resp
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Kunne ikke parse AI Gateway-respons: {e}"))?;
|
||||||
|
|
||||||
|
let content = chat_resp
|
||||||
|
.choices
|
||||||
|
.first()
|
||||||
|
.and_then(|c| c.message.content.as_deref())
|
||||||
|
.ok_or("AI Gateway returnerte ingen content")?;
|
||||||
|
|
||||||
|
Ok((content.to_string(), chat_resp.usage, chat_resp.model))
|
||||||
|
}
|
||||||
|
|
@ -3228,6 +3228,134 @@ pub async fn summarize(
|
||||||
Ok(Json(SummarizeResponse { job_id }))
|
Ok(Json(SummarizeResponse { job_id }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// POST /intentions/ai_process — AI-prosessering via AI Gateway
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct AiProcessRequest {
|
||||||
|
/// Kilde-noden som skal prosesseres.
|
||||||
|
pub source_node_id: Uuid,
|
||||||
|
/// AI-preset som definerer prompt og modellprofil.
|
||||||
|
pub ai_preset_id: Uuid,
|
||||||
|
/// Retning: "node_to_tool" (opprett ny node) eller "tool_to_node" (modifiser in-place).
|
||||||
|
pub direction: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct AiProcessResponse {
|
||||||
|
pub job_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// POST /intentions/ai_process
|
||||||
|
///
|
||||||
|
/// Legger en `ai_process`-jobb i køen.
|
||||||
|
/// AI-prosesseringen skjer asynkront — kilde-content sendes til AI Gateway
|
||||||
|
/// med preset-prompt, og forbruk logges i ai_usage_log.
|
||||||
|
///
|
||||||
|
/// Direction-logikk (opprett ny node vs. oppdater eksisterende) implementeres
|
||||||
|
/// i oppgave 18.3.
|
||||||
|
///
|
||||||
|
/// Ref: docs/features/ai_verktoy.md § 6.1
|
||||||
|
pub async fn ai_process(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
user: AuthUser,
|
||||||
|
Json(req): Json<AiProcessRequest>,
|
||||||
|
) -> Result<Json<AiProcessResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||||
|
// Valider direction
|
||||||
|
if req.direction != "node_to_tool" && req.direction != "tool_to_node" {
|
||||||
|
return Err(bad_request(
|
||||||
|
"direction må være 'node_to_tool' eller 'tool_to_node'",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sjekk at kilde-noden finnes
|
||||||
|
let source_exists: bool = sqlx::query_scalar::<_, bool>(
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM nodes WHERE id = $1)",
|
||||||
|
)
|
||||||
|
.bind(req.source_node_id)
|
||||||
|
.fetch_one(&state.db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!(error = %e, "PG-feil ved kilde-node-sjekk");
|
||||||
|
internal_error("Databasefeil")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !source_exists {
|
||||||
|
return Err(bad_request("Kilde-node finnes ikke"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sjekk at AI-preset finnes
|
||||||
|
let preset_exists: bool = sqlx::query_scalar::<_, bool>(
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM nodes WHERE id = $1 AND node_kind = 'ai_preset')",
|
||||||
|
)
|
||||||
|
.bind(req.ai_preset_id)
|
||||||
|
.fetch_one(&state.db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!(error = %e, "PG-feil ved preset-sjekk");
|
||||||
|
internal_error("Databasefeil")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !preset_exists {
|
||||||
|
return Err(bad_request("AI-preset finnes ikke"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// For tool_to_node-retning trengs skrivetilgang til kilde-noden
|
||||||
|
if req.direction == "tool_to_node" {
|
||||||
|
let can_modify = user_can_modify_node(&state.db, user.node_id, req.source_node_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!(error = %e, "PG-feil ved tilgangssjekk");
|
||||||
|
internal_error("Databasefeil")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !can_modify {
|
||||||
|
return Err(forbidden(
|
||||||
|
"Ingen tilgang til å endre kilde-noden (tool_to_node krever skrivetilgang)",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finn samlings-ID for kilde-noden (for prioritering)
|
||||||
|
let collection_id = crate::resource_usage::find_collection_for_node(
|
||||||
|
&state.db,
|
||||||
|
req.source_node_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let payload = serde_json::json!({
|
||||||
|
"source_node_id": req.source_node_id.to_string(),
|
||||||
|
"ai_preset_id": req.ai_preset_id.to_string(),
|
||||||
|
"direction": req.direction,
|
||||||
|
"requested_by": user.node_id.to_string()
|
||||||
|
});
|
||||||
|
|
||||||
|
let job_id = crate::jobs::enqueue(
|
||||||
|
&state.db,
|
||||||
|
"ai_process",
|
||||||
|
payload,
|
||||||
|
collection_id,
|
||||||
|
5, // Medium prioritet
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!(error = %e, "Kunne ikke legge ai_process-jobb i kø");
|
||||||
|
internal_error("Kunne ikke starte AI-prosessering")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
job_id = %job_id,
|
||||||
|
source_node_id = %req.source_node_id,
|
||||||
|
ai_preset_id = %req.ai_preset_id,
|
||||||
|
direction = %req.direction,
|
||||||
|
user = %user.node_id,
|
||||||
|
"ai_process-jobb lagt i kø"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Json(AiProcessResponse { job_id }))
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// POST /intentions/generate_tts — tekst-til-tale via ElevenLabs
|
// POST /intentions/generate_tts — tekst-til-tale via ElevenLabs
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ use uuid::Uuid;
|
||||||
|
|
||||||
use crate::agent;
|
use crate::agent;
|
||||||
use crate::ai_edges;
|
use crate::ai_edges;
|
||||||
|
use crate::ai_process;
|
||||||
use crate::audio;
|
use crate::audio;
|
||||||
use crate::cas::CasStore;
|
use crate::cas::CasStore;
|
||||||
use crate::maintenance::MaintenanceState;
|
use crate::maintenance::MaintenanceState;
|
||||||
|
|
@ -184,6 +185,9 @@ async fn dispatch(
|
||||||
"audio_process" => {
|
"audio_process" => {
|
||||||
audio::handle_audio_process_job(job, db, stdb, cas).await
|
audio::handle_audio_process_job(job, db, stdb, cas).await
|
||||||
}
|
}
|
||||||
|
"ai_process" => {
|
||||||
|
ai_process::handle_ai_process(job, db).await
|
||||||
|
}
|
||||||
"render_article" => {
|
"render_article" => {
|
||||||
handle_render_article(job, db, cas).await
|
handle_render_article(job, db, cas).await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
pub mod agent;
|
pub mod agent;
|
||||||
pub mod ai_admin;
|
pub mod ai_admin;
|
||||||
pub mod ai_edges;
|
pub mod ai_edges;
|
||||||
|
pub mod ai_process;
|
||||||
pub mod audio;
|
pub mod audio;
|
||||||
pub mod bandwidth;
|
pub mod bandwidth;
|
||||||
mod auth;
|
mod auth;
|
||||||
|
|
@ -202,6 +203,7 @@ async fn main() {
|
||||||
.route("/intentions/retranscribe", post(intentions::retranscribe))
|
.route("/intentions/retranscribe", post(intentions::retranscribe))
|
||||||
.route("/intentions/resolve_retranscription", post(intentions::resolve_retranscription))
|
.route("/intentions/resolve_retranscription", post(intentions::resolve_retranscription))
|
||||||
.route("/intentions/summarize", post(intentions::summarize))
|
.route("/intentions/summarize", post(intentions::summarize))
|
||||||
|
.route("/intentions/ai_process", post(intentions::ai_process))
|
||||||
.route("/intentions/generate_tts", post(intentions::generate_tts))
|
.route("/intentions/generate_tts", post(intentions::generate_tts))
|
||||||
.route("/intentions/join_communication", post(intentions::join_communication))
|
.route("/intentions/join_communication", post(intentions::join_communication))
|
||||||
.route("/intentions/leave_communication", post(intentions::leave_communication))
|
.route("/intentions/leave_communication", post(intentions::leave_communication))
|
||||||
|
|
|
||||||
3
tasks.md
3
tasks.md
|
|
@ -202,8 +202,7 @@ Ref: Kodegjennomgang av `b4c4bb8` (Lydstudio: lydredigering via FFmpeg).
|
||||||
Ref: `docs/features/ai_verktoy.md`, `docs/retninger/arbeidsflaten.md`
|
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.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).
|
||||||
- [~] 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.
|
- [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.
|
||||||
> Påbegynt: 2026-03-18T06:14
|
|
||||||
- [ ] 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.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.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.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.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue