graphify_extract/semantic/
anthropic.rs1use std::path::Path;
2
3use anyhow::{Context, Result};
4use graphify_core::model::ExtractionResult;
5use serde::{Deserialize, Serialize};
6
7use super::provider::{AuthType, LLMProviderConfig};
8
9#[derive(Serialize)]
10struct MessageRequest {
11 model: String,
12 max_tokens: u32,
13 messages: Vec<AnthropicMessage>,
14 system: String,
15}
16
17#[derive(Serialize)]
18struct AnthropicMessage {
19 role: String,
20 content: String,
21}
22
23#[derive(Deserialize)]
24struct MessageResponse {
25 content: Vec<ContentBlock>,
26}
27
28#[derive(Deserialize)]
29struct ContentBlock {
30 text: Option<String>,
31}
32
33pub async fn extract_anthropic(
34 path: &Path,
35 content: &str,
36 file_type: &str,
37 config: &LLMProviderConfig,
38) -> Result<ExtractionResult> {
39 let file_str = path.to_string_lossy();
40 let system_prompt = super::build_system_prompt(file_type);
41 let user_prompt = super::build_user_prompt(content, file_type);
42
43 let request_body = MessageRequest {
44 model: config.model.clone(),
45 max_tokens: 4096,
46 messages: vec![AnthropicMessage {
47 role: "user".to_string(),
48 content: user_prompt,
49 }],
50 system: system_prompt,
51 };
52
53 let client = reqwest::Client::new();
54 let mut request = client
55 .post(format!("{}/v1/messages", config.base_url))
56 .header("anthropic-version", "2023-06-01")
57 .header("content-type", "application/json")
58 .json(&request_body);
59
60 match config.auth_type {
61 AuthType::ApiKey => {
62 if let Some(ref key) = config.api_key {
63 request = request.header("x-api-key", key);
64 } else {
65 anyhow::bail!(
66 "No API key configured for Anthropic. \
67 Set ANTHROPIC_API_KEY or configure [llm] in graphify.toml"
68 );
69 }
70 }
71 AuthType::Bearer => {
72 if let Some(ref token) = config.api_key {
73 request = request.header("authorization", format!("Bearer {token}"));
74 } else {
75 anyhow::bail!(
76 "No OAuth token configured for Anthropic. \
77 Run `claude login` or configure [llm] in graphify.toml"
78 );
79 }
80 }
81 }
82
83 let response = request
84 .send()
85 .await
86 .context("failed to send request to Anthropic API")?;
87
88 if response.status().as_u16() == 401 {
89 anyhow::bail!(
90 "Anthropic API key invalid or OAuth token expired. \
91 Run `claude login` to refresh, or set ANTHROPIC_API_KEY."
92 );
93 }
94
95 if response.status().as_u16() == 400 || response.status().as_u16() == 404 {
96 let status = response.status();
97 let body = response.text().await.unwrap_or_default();
98 anyhow::bail!(
99 "Model '{}' not found. Check available models at docs.anthropic.com\nAPI returned {}: {}",
100 config.model,
101 status,
102 body
103 );
104 }
105
106 if !response.status().is_success() {
107 let status = response.status();
108 let body = response.text().await.unwrap_or_default();
109 anyhow::bail!("Anthropic API returned {status}: {body}");
110 }
111
112 let msg: MessageResponse = response
113 .json()
114 .await
115 .context("failed to parse Anthropic API response")?;
116
117 let text = msg
118 .content
119 .first()
120 .and_then(|b| b.text.as_deref())
121 .unwrap_or("{}");
122
123 super::parse_semantic_response(text, &file_str)
124}