1use anyhow::Result;
8use regex;
9use std::path::PathBuf;
10
11use crate::llm::Message;
12use crate::wiki::Wiki;
13
14#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
16pub struct QueryResult {
17 pub ok: bool,
18 pub question: String,
19 pub answer: String,
20 pub sources: Vec<String>,
22 #[serde(default)]
24 pub no_wiki_content: bool,
25}
26
27impl QueryResult {
28 pub fn ok(question: String, answer: String, sources: Vec<PathBuf>) -> Self {
29 Self {
30 ok: true,
31 question,
32 answer,
33 sources: sources
34 .into_iter()
35 .map(|p| p.to_string_lossy().to_string())
36 .collect(),
37 no_wiki_content: false,
38 }
39 }
40
41 pub fn empty(question: String) -> Self {
42 Self {
43 ok: true,
44 question,
45 answer: String::new(),
46 sources: vec![],
47 no_wiki_content: true,
48 }
49 }
50}
51
52pub async fn run(wiki: &Wiki, question: &str) -> Result<QueryResult> {
54 let files = wiki.read_wiki_files()?;
55
56 if files.is_empty() {
57 return Ok(QueryResult::empty(question.to_string()));
58 }
59
60 let context = build_context(&files);
62 let system_md = wiki.config().system_md_content()?;
63 let prompt = build_query_prompt(&system_md, question, &context);
64
65 let llm = wiki.llm();
66 let messages = &[Message::system(&prompt)];
67 let answer = llm.chat(messages).await?;
68 let answer = strip_think_tags(&answer);
69
70 let sources: Vec<PathBuf> = files.iter().map(|f| f.path.clone()).collect();
71 Ok(QueryResult::ok(question.to_string(), answer, sources))
72}
73
74fn strip_think_tags(s: &str) -> String {
76 let re = regex::Regex::new(r"(?s)<think>.*?</think>").unwrap();
77 re.replace_all(s, "").to_string()
78}
79
80fn build_context(files: &[crate::wiki::WikiFile]) -> String {
82 if files.is_empty() {
83 return String::from("(知识库为空)");
84 }
85
86 let mut ctx = String::new();
87
88 for file in files {
89 let _filename = file
90 .path
91 .file_name()
92 .and_then(|n| n.to_str())
93 .unwrap_or("unknown");
94
95 ctx.push_str("## 文件: \n\n");
96 ctx.push_str(&file.content);
97 ctx.push_str("\n\n---\n\n");
98 }
99
100 ctx
101}
102
103fn build_query_prompt(system_md: &str, question: &str, context: &str) -> String {
105 format!(
106 r#"# System Prompt
107
108{system_md}
109
110---
111
112# Task
113
114你是一个知识库助手。请根据以下知识库内容回答用户的问题。
115
116## 知识库内容
117{context}
118
119---
120
121## 用户问题
122
123{question}
124
125---
126
127## 要求
128
1291. 优先使用知识库中的信息回答
1302. 如果知识库中没有相关信息,直接说明"知识库中没有相关内容"
1313. 用中文回答
1324. 如有参考,提及来源文件名
1335. 简洁准确,不得编造信息
134"#
135 )
136}