2 files+45−40/2 viewed
| 1 | + | -- Triage suggestions the first deploy of the plan's upkeep asked for and | |
| 2 | + | -- could not read (items/plan_settle.rs read only four lines then, and kept | |
| 3 | + | -- no answer it could not read): asked again, once, now that a JSON answer | |
| 4 | + | -- reads too. Only open work items still in Triage with no suggestion. | |
| 5 | + | UPDATE work_items SET suggested_at = NULL | |
| 6 | + | WHERE state = 'open' AND COALESCE(horizon, 'triage') = 'triage' AND suggestion IS NULL AND suggested_at IS NOT NULL; |
| 16 | 16 | ||
| 17 | 17 | use std::collections::HashSet; | |
| 18 | 18 | ||
| 19 | − | use g1t_contracts::ai::{AiBriefArgs, MAX_BRIEF_INPUT_CHARS}; | |
| 19 | + | use g1t_contracts::ai::{AiBriefArgs, MAX_BRIEF_INPUT_CHARS, MAX_BRIEF_TOKENS}; | |
| 20 | 20 | use g1t_contracts::events::WorkItemEvent; | |
| 21 | 21 | use g1t_contracts::time::rfc3339; | |
| 22 | 22 | use g1t_contracts::work::*; | |
| ⋯ | |||
| 64 | 64 | /// say a horizon it may suggest. `known` are the open work items it may | |
| 65 | 65 | /// say this one repeats. | |
| 66 | 66 | pub(crate) fn read_suggestion(answer: &str, known: &[u32], at: &str) -> Option<PlanSuggestion> { | |
| 67 | + | // Some models answer with a JSON object instead of the four lines. | |
| 68 | + | let json = answer.find('{').zip(answer.rfind('}')).and_then(|(open, close)| serde_json::from_str::<serde_json::Value>(&answer[open..=close]).ok()); | |
| 69 | + | if let Some(serde_json::Value::Object(fields)) = json { | |
| 70 | + | let text = |key: &str| match fields.get(key) { | |
| 71 | + | Some(serde_json::Value::String(text)) => text.clone(), | |
| 72 | + | Some(serde_json::Value::Number(number)) => number.to_string(), | |
| 73 | + | _ => String::new(), | |
| 74 | + | }; | |
| 75 | + | let lines = format!( | |
| 76 | + | "horizon: {}\nappetite: {}\nduplicate: {}\nwhy: {}", | |
| 77 | + | text("horizon"), | |
| 78 | + | text("appetite"), | |
| 79 | + | if text("duplicate").is_empty() { text("duplicate_of") } else { text("duplicate") }, | |
| 80 | + | text("why") | |
| 81 | + | ); | |
| 82 | + | return read_lines(&lines, known, at); | |
| 83 | + | } | |
| 84 | + | read_lines(answer, known, at) | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | /// [`read_suggestion`] of the four `key: value` lines. | |
| 88 | + | fn read_lines(answer: &str, known: &[u32], at: &str) -> Option<PlanSuggestion> { | |
| 67 | 89 | let mut horizon = None; | |
| 68 | 90 | let mut appetite = None; | |
| 69 | 91 | let mut duplicate_of = None; | |
| ⋯ | |||
| 73 | 95 | continue; | |
| 74 | 96 | }; | |
| 75 | 97 | let value = value.trim().trim_matches(|c: char| c == '*' || c == '`' || c == '"').trim(); | |
| 76 | − | match key.trim().trim_matches(|c: char| c == '*' || c == '-').trim().to_ascii_lowercase().as_str() { | |
| 98 | + | match key.trim().trim_matches(|c: char| c == '*' || c == '-' || c == '#' || c == '"').trim().to_ascii_lowercase().as_str() { | |
| 77 | 99 | "horizon" => horizon = Horizon::parse(value).filter(|h| matches!(h, Horizon::Now | Horizon::Next | Horizon::Later)), | |
| 78 | 100 | "appetite" => appetite = Appetite::parse(value), | |
| 79 | 101 | "duplicate" => { | |
| ⋯ | |||
| 170 | 192 | match g1t_kit::call::<_, Outcome<Option<String>>>(integrations, "ai_brief", &args).await { | |
| 171 | 193 | Ok(Outcome::Ok(Some(answer))) => Some(answer), | |
| 172 | 194 | Ok(Outcome::Ok(None)) => { | |
| 195 | + | worker::console_log!("{workspace}: the plan got no answer from the AI provider (none chosen, or an empty answer)"); | |
| 173 | 196 | budget.without.insert(workspace.to_owned()); | |
| 174 | 197 | None | |
| 175 | 198 | } | |
| ⋯ | |||
| 281 | 304 | listed.join("\n") | |
| 282 | 305 | ); | |
| 283 | 306 | let at = rfc3339(now_ms()); | |
| 284 | − | let answer = self.brief(&item.workspace, SUGGEST_SYSTEM, input, 120, budget).await; | |
| 307 | + | let answer = self.brief(&item.workspace, SUGGEST_SYSTEM, input, MAX_BRIEF_TOKENS, budget).await; | |
| 285 | 308 | if answer.is_none() && !budget.without.contains(&item.workspace) { | |
| 286 | 309 | // Out of calls for this run: asked again on the next. | |
| 287 | 310 | break; | |
| 288 | 311 | } | |
| 289 | 312 | let suggestion = answer.as_deref().and_then(|answer| read_suggestion(answer, &known, &at)); | |
| 313 | + | if let (Some(answer), None) = (&answer, &suggestion) { | |
| 314 | + | worker::console_log!("{}#{}: the AI provider's suggestion did not read: {}", item.workspace, item.number, clip(answer, 300)); | |
| 315 | + | } | |
| 290 | 316 | // Asked once: with no provider, or no usable answer, it is not | |
| 291 | 317 | // asked again. | |
| 292 | 318 | self.db | |
| ⋯ | |||
| 365 | 391 | if decisions.is_empty() { none() } else { decisions.join("\n") }, | |
| 366 | 392 | if pulls.is_empty() { none() } else { pulls.join("\n") }, | |
| 367 | 393 | ); | |
| 368 | − | let answer = self.brief(&item.workspace, STANDING_SYSTEM, input, 120, budget).await; | |
| 394 | + | let answer = self.brief(&item.workspace, STANDING_SYSTEM, input, MAX_BRIEF_TOKENS, budget).await; | |
| 369 | 395 | if answer.is_none() && !budget.without.contains(&item.workspace) { | |
| 370 | 396 | break; | |
| 371 | 397 | } | |
| ⋯ | |||
| 405 | 431 | // A horizon it may not suggest, or none: no suggestion. | |
| 406 | 432 | assert_eq!(read_suggestion("horizon: set_aside\nwhy: old", &[], at), None); | |
| 407 | 433 | assert_eq!(read_suggestion("I think this should go next.", &[], at), None); | |
| 434 | + | // A JSON object, in a code fence or not. | |
| 435 | + | let json = "```json\n{\"horizon\": \"next\", \"appetite\": \"medium\", \"duplicate\": 12, \"why\": \"Same as #12.\"}\n```"; | |
| 436 | + | let read = read_suggestion(json, &[12], at).unwrap(); | |
| 437 | + | assert_eq!((read.horizon, read.appetite, read.duplicate_of, read.why.as_str()), (Horizon::Next, Some(Appetite::Medium), Some(12), "Same as #12.")); | |
| 438 | + | let read = read_suggestion("{\"horizon\":\"later\",\"appetite\":\"small\",\"duplicate\":null,\"why\":\"Nice to have.\"}", &[], at).unwrap(); | |
| 439 | + | assert_eq!((read.horizon, read.duplicate_of), (Horizon::Later, None)); | |
| 440 | + | // Keys in quotes or headings. | |
| 441 | + | let read = read_suggestion("## Horizon: next\n\"appetite\": small\nwhy: ok", &[], at).unwrap(); | |
| 442 | + | assert_eq!((read.horizon, read.appetite), (Horizon::Next, Some(Appetite::Small))); | |
| 408 | 443 | } | |
| 409 | 444 | ||
| 410 | 445 | #[test] | |
Comments
Sign in to comment.