git_iris/agents/tools/
docs.rs1use anyhow::Result;
7use rig::completion::ToolDefinition;
8use rig::tool::Tool;
9use serde::{Deserialize, Serialize};
10use std::path::PathBuf;
11
12use super::common::parameters_schema;
13
14crate::define_tool_error!(DocsError);
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ProjectDocs;
20
21#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema, Default)]
23#[serde(rename_all = "lowercase")]
24pub enum DocType {
25 #[default]
27 Readme,
28 Contributing,
30 Changelog,
32 License,
34 CodeOfConduct,
36 Agents,
38 Context,
40 All,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
45pub struct ProjectDocsArgs {
46 #[serde(default)]
48 pub doc_type: DocType,
49 #[serde(default = "default_max_chars")]
51 pub max_chars: usize,
52}
53
54fn default_max_chars() -> usize {
55 5000
56}
57
58impl Tool for ProjectDocs {
59 const NAME: &'static str = "project_docs";
60 type Error = DocsError;
61 type Args = ProjectDocsArgs;
62 type Output = String;
63
64 async fn definition(&self, _: String) -> ToolDefinition {
65 ToolDefinition {
66 name: "project_docs".to_string(),
67 description:
68 "Fetch project documentation for context. Types: readme, contributing, changelog, license, codeofconduct, agents (AGENTS.md/CLAUDE.md), context (readme + agent instructions - RECOMMENDED), all"
69 .to_string(),
70 parameters: parameters_schema::<ProjectDocsArgs>(),
71 }
72 }
73
74 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
75 let current_dir = std::env::current_dir().map_err(DocsError::from)?;
76 let max_chars = args.max_chars.min(20000);
77
78 let files_to_check = match args.doc_type {
79 DocType::Readme => vec![
80 "README.md",
81 "README.rst",
82 "README.txt",
83 "README",
84 "readme.md",
85 ],
86 DocType::Contributing => vec!["CONTRIBUTING.md", "CONTRIBUTING", "contributing.md"],
87 DocType::Changelog => vec![
88 "CHANGELOG.md",
89 "CHANGELOG",
90 "HISTORY.md",
91 "CHANGES.md",
92 "changelog.md",
93 ],
94 DocType::License => vec!["LICENSE", "LICENSE.md", "LICENSE.txt", "license"],
95 DocType::CodeOfConduct => vec!["CODE_OF_CONDUCT.md", "code_of_conduct.md"],
96 DocType::Agents => vec![
97 "AGENTS.md",
98 "CLAUDE.md",
99 ".github/copilot-instructions.md",
100 ".cursor/rules",
101 "CODING_GUIDELINES.md",
102 ],
103 DocType::Context => vec![
104 "README.md",
105 "AGENTS.md",
106 "CLAUDE.md",
107 ".github/copilot-instructions.md",
108 ],
109 DocType::All => vec![
110 "README.md",
111 "AGENTS.md",
112 "CLAUDE.md",
113 "CONTRIBUTING.md",
114 "CHANGELOG.md",
115 "CODE_OF_CONDUCT.md",
116 ],
117 };
118
119 let mut output = String::new();
120 let mut found_any = false;
121 let mut found_agent_doc = false;
123
124 for filename in files_to_check {
125 if filename == "CLAUDE.md" && found_agent_doc {
127 continue;
128 }
129
130 let path: PathBuf = current_dir.join(filename);
131 if path.exists() {
132 match tokio::fs::read_to_string(&path).await {
133 Ok(content) => {
134 found_any = true;
135
136 if filename == "AGENTS.md" {
138 found_agent_doc = true;
139 }
140
141 output.push_str(&format!("=== {} ===\n", filename));
142
143 let char_count = content.chars().count();
145 if char_count > max_chars {
146 let truncated: String = content.chars().take(max_chars).collect();
147 output.push_str(&truncated);
148 output.push_str(&format!(
149 "\n\n[... truncated, {} more chars ...]\n",
150 char_count - max_chars
151 ));
152 } else {
153 output.push_str(&content);
154 }
155 output.push_str("\n\n");
156
157 if !matches!(args.doc_type, DocType::All | DocType::Context) {
160 break;
161 }
162 }
163 Err(e) => {
164 output.push_str(&format!("Error reading {}: {}\n", filename, e));
165 }
166 }
167 }
168 }
169
170 if !found_any {
171 output = format!(
172 "No {:?} documentation found in project root.",
173 args.doc_type
174 );
175 }
176
177 Ok(output)
178 }
179}