diff --git a/crates/llm/src/grammars.rs b/crates/llm/src/grammars.rs index 135073b..4008d04 100644 --- a/crates/llm/src/grammars.rs +++ b/crates/llm/src/grammars.rs @@ -1,3 +1,18 @@ +// Phase 9 content-tag extraction. Restricts the model output to a +// strict {topic, intent} JSON object where topic is a lowercase +// hyphen-joined slug of at least 3 chars (no upper bound is encoded +// in the grammar — max_tokens caps it in practice) and intent is one +// of the six closed-set values. Recursive `topic-rest` keeps the +// shape compatible with the existing GBNF style in this file. +pub const CONTENT_TAGS_GRAMMAR: &str = r##" +root ::= "{" ws "\"topic\":" ws topic-str ws "," ws "\"intent\":" ws intent ws "}" ws +topic-str ::= "\"" topic-char topic-char topic-char topic-rest "\"" +topic-rest ::= "" | topic-char topic-rest +topic-char ::= [a-z0-9-] +intent ::= "\"planning\"" | "\"reflection\"" | "\"venting\"" | "\"capture\"" | "\"decision\"" | "\"question\"" +ws ::= ([ \t\n] ws)? +"##; + pub const TASK_ARRAY_GRAMMAR: &str = r#" root ::= "[" ws string ws "," ws string ws "," ws string rest3 ws "]" rest3 ::= "" | "," ws string rest4 diff --git a/crates/llm/src/lib.rs b/crates/llm/src/lib.rs index ce03857..99c9585 100644 --- a/crates/llm/src/lib.rs +++ b/crates/llm/src/lib.rs @@ -15,7 +15,9 @@ pub mod grammars; pub mod model_manager; pub mod prompts; +pub use grammars::CONTENT_TAGS_GRAMMAR; pub use model_manager::{recommend_tier, LlmModelId, LlmModelInfo}; +pub use prompts::{is_valid_intent, ContentTags, CONTENT_TAGS_SYSTEM, INTENT_CLOSED_SET}; const DEFAULT_CONTEXT_TOKENS: u32 = 4096; const MAX_CONTEXT_TOKENS: u32 = 8192; diff --git a/crates/llm/src/prompts.rs b/crates/llm/src/prompts.rs index 0a1a61b..5e9ba13 100644 --- a/crates/llm/src/prompts.rs +++ b/crates/llm/src/prompts.rs @@ -4,6 +4,39 @@ between 3 and 7 concrete, physical micro-steps. Each step must be a short \ imperative sentence, actionable today, with no commentary. Output ONLY a \ JSON array of strings."; +// Phase 9 content-tag extraction. The model emits a {topic, intent} +// JSON pair under a strict GBNF (see grammars::CONTENT_TAGS_GRAMMAR). +// CONTENT_TAGS_SYSTEM is the system message; the user message wraps +// the transcript text. +pub const CONTENT_TAGS_SYSTEM: &str = "\ +You tag a transcript with ONE topic and ONE intent. \ +TOPIC is a 1 to 3 token lowercase hyphen-joined noun phrase naming the \ +dominant subject. Examples: interview-prep, grant-application, \ +daily-standup. \ +INTENT is exactly one of: planning, reflection, venting, capture, \ +decision, question. \ +Return JSON only, with this exact shape: \ +{\"topic\":\"...\",\"intent\":\"...\"}"; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ContentTags { + pub topic: String, + pub intent: String, +} + +pub const INTENT_CLOSED_SET: &[&str] = &[ + "planning", + "reflection", + "venting", + "capture", + "decision", + "question", +]; + +pub fn is_valid_intent(s: &str) -> bool { + INTENT_CLOSED_SET.contains(&s) +} + pub const EXTRACT_TASKS_SYSTEM: &str = "\ You are a task-extraction assistant. Given a transcript of spoken notes, \ output a JSON array of action items the speaker committed to. Each item must \