Skip to main content

graphify_extract/semantic/
anthropic.rs

1use 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}