Skip to main content

lc_agents/deep_research/
types.rs

1// lc-agents/src/deep_research/types.rs
2//! Public data types (citation / report) and the gap→query parser.
3
4use super::planner;
5
6/// A citation referencing a source used in the research report.
7#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
8pub struct Citation {
9    /// 1-based citation index used in the report body (e.g. \[1\]).
10    pub index: usize,
11    /// Human-readable source title or description.
12    pub source: String,
13    /// Optional URL for the source.
14    pub url: Option<String>,
15    /// Short snippet quoted or paraphrased from the source.
16    pub snippet: String,
17}
18
19/// The final output of a deep research run.
20#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
21pub struct ResearchReport {
22    /// The full report in markdown format with inline citation markers.
23    pub markdown: String,
24    /// Ordered list of citations referenced in the report.
25    pub citations: Vec<Citation>,
26    /// Sub-topics that were investigated.
27    pub subtopics: Vec<String>,
28    /// Number of research rounds completed.
29    pub rounds_completed: usize,
30}
31
32/// Parses the LLM output for gap→query mapping into a flat list of queries.
33///
34/// Expected format: `[{"gap": "...", "queries": ["q1", "q2"]}, ...]`
35///
36/// Validates that every gap in the input list has at least one corresponding query.
37/// If a gap has no queries in the parsed output, a warning is logged and a
38/// fallback query is generated from the gap text itself.
39pub(crate) fn parse_gap_queries(
40    content: &str,
41    original_gaps: &[String],
42) -> Result<Vec<String>, super::ResearchError> {
43    #[derive(serde::Deserialize)]
44    struct GapMapping {
45        gap: String,
46        queries: Vec<String>,
47    }
48
49    let json_str = planner::extract_json(content);
50    let mappings: Vec<GapMapping> = serde_json::from_str(&json_str).map_err(|e| {
51        let preview: String = content.chars().take(200).collect();
52        super::ResearchError::Llm(format!(
53            "failed to parse gap→query mapping: {} | raw: {}",
54            e, preview
55        ))
56    })?;
57
58    // Collect all queries, ensuring each original gap is covered.
59    let mut all_queries: Vec<String> = Vec::new();
60    let mut covered_gaps: std::collections::HashSet<usize> = std::collections::HashSet::new();
61
62    for mapping in &mappings {
63        // Check if this mapping corresponds to any original gap (fuzzy match by substring)
64        for (i, gap) in original_gaps.iter().enumerate() {
65            if !covered_gaps.contains(&i)
66                && (mapping.gap.contains(gap.as_str()) || gap.contains(mapping.gap.as_str()))
67            {
68                covered_gaps.insert(i);
69            }
70        }
71        all_queries.extend(mapping.queries.iter().filter(|q| !q.is_empty()).cloned());
72    }
73
74    // For any uncovered gap, generate a fallback query from the gap text itself.
75    for (i, gap) in original_gaps.iter().enumerate() {
76        if !covered_gaps.contains(&i) {
77            log::warn!(
78                "Deep Research: gap '{}' has no follow-up queries, using gap as query",
79                gap
80            );
81            all_queries.push(gap.clone());
82        }
83    }
84
85    Ok(all_queries)
86}