Coverage for src / dataknobs_llm / llm / providers / anthropic.py: 14%
112 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-15 10:29 -0700
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-15 10:29 -0700
1"""Anthropic Claude LLM provider implementation.
3This module provides Anthropic Claude API integration for dataknobs-llm, supporting:
4- Claude 3 (Opus, Sonnet, Haiku) and Claude 2 models
5- Native tools API for function calling
6- Vision capabilities (Claude 3+)
7- Streaming responses
8- Long context windows (up to 200k tokens)
9- Advanced reasoning and coding capabilities
11The AnthropicProvider uses the official Anthropic Python SDK and supports
12all standard Anthropic API parameters including system prompts, temperature,
13and token limits.
15Example:
16 ```python
17 from dataknobs_llm.llm.providers import AnthropicProvider
18 from dataknobs_llm.llm.base import LLMConfig
20 # Create provider
21 config = LLMConfig(
22 provider="anthropic",
23 model="claude-3-sonnet-20240229",
24 api_key="sk-ant-...", # or set ANTHROPIC_API_KEY env var
25 temperature=0.7,
26 max_tokens=1024
27 )
29 async with AnthropicProvider(config) as llm:
30 # Simple completion
31 response = await llm.complete("Explain quantum computing")
32 print(response.content)
34 # Streaming for real-time output
35 async for chunk in llm.stream_complete("Write a story"):
36 print(chunk.delta, end="", flush=True)
38 # Tool use (Claude 3+)
39 tools = [{
40 "name": "calculator",
41 "description": "Perform arithmetic",
42 "input_schema": {
43 "type": "object",
44 "properties": {
45 "operation": {"type": "string"},
46 "x": {"type": "number"},
47 "y": {"type": "number"}
48 }
49 }
50 }]
52 response = await llm.function_call(messages, tools)
53 ```
55See Also:
56 - Anthropic API Documentation: https://docs.anthropic.com/
57 - anthropic Python package: https://github.com/anthropics/anthropic-sdk-python
58"""
60import os
61import json
62from typing import TYPE_CHECKING, Any, Dict, List, Union, AsyncIterator
64from ..base import (
65 LLMConfig, LLMMessage, LLMResponse, LLMStreamResponse,
66 AsyncLLMProvider, ModelCapability,
67 normalize_llm_config
68)
69from dataknobs_llm.prompts import AsyncPromptBuilder
71if TYPE_CHECKING:
72 from dataknobs_config.config import Config
75class AnthropicProvider(AsyncLLMProvider):
76 r"""Anthropic Claude LLM provider with full API support.
78 Provides async access to Anthropic's Claude models including Claude 3
79 (Opus, Sonnet, Haiku) and Claude 2. Supports advanced features like
80 native tool use, vision, and extended context windows.
82 Features:
83 - Claude 3 Opus/Sonnet/Haiku and Claude 2 models
84 - Native tools API for function calling (Claude 3+)
85 - Vision capabilities for image understanding (Claude 3+)
86 - Streaming responses for real-time output
87 - Long context windows (up to 200k tokens)
88 - Advanced reasoning and coding capabilities
89 - System prompts for behavior control
90 - JSON output mode
92 Example:
93 ```python
94 from dataknobs_llm.llm.providers import AnthropicProvider
95 from dataknobs_llm.llm.base import LLMConfig, LLMMessage
97 # Basic usage
98 config = LLMConfig(
99 provider="anthropic",
100 model="claude-3-sonnet-20240229",
101 api_key="sk-ant-...",
102 temperature=0.7,
103 max_tokens=1024
104 )
106 async with AnthropicProvider(config) as llm:
107 # Simple completion
108 response = await llm.complete("Explain machine learning")
109 print(response.content)
111 # With system prompt
112 messages = [
113 LLMMessage(
114 role="system",
115 content="You are an expert Python tutor"
116 ),
117 LLMMessage(
118 role="user",
119 content="How do I use decorators?"
120 )
121 ]
122 response = await llm.complete(messages)
124 # Long context processing (Claude 3+)
125 long_config = LLMConfig(
126 provider="anthropic",
127 model="claude-3-opus-20240229",
128 max_tokens=4096
129 )
131 llm = AnthropicProvider(long_config)
132 await llm.initialize()
134 # Process large document
135 with open("large_doc.txt") as f:
136 long_text = f.read() # Up to 200k tokens!
138 response = await llm.complete(
139 f"Summarize this document:\n\n{long_text}"
140 )
142 # Tool use / function calling (Claude 3+)
143 tools = [
144 {
145 "name": "web_search",
146 "description": "Search the web for information",
147 "input_schema": {
148 "type": "object",
149 "properties": {
150 "query": {
151 "type": "string",
152 "description": "Search query"
153 },
154 "num_results": {
155 "type": "integer",
156 "description": "Number of results"
157 }
158 },
159 "required": ["query"]
160 }
161 }
162 ]
164 messages = [
165 LLMMessage(
166 role="user",
167 content="Search for latest AI news"
168 )
169 ]
171 response = await llm.function_call(messages, tools)
172 if response.function_call:
173 import json
174 tool_input = json.loads(response.function_call["arguments"])
175 print(f"Tool: {response.function_call['name']}")
176 print(f"Input: {tool_input}")
177 ```
179 Args:
180 config: LLMConfig, dataknobs Config, or dict with provider settings
181 prompt_builder: Optional AsyncPromptBuilder for prompt rendering
183 Attributes:
184 _client: Anthropic AsyncAnthropic client instance
186 See Also:
187 LLMConfig: Configuration options
188 AsyncLLMProvider: Base provider interface
189 Anthropic API Docs: https://docs.anthropic.com/
190 """
192 def __init__(
193 self,
194 config: Union[LLMConfig, "Config", Dict[str, Any]],
195 prompt_builder: AsyncPromptBuilder | None = None
196 ):
197 # Normalize config first
198 llm_config = normalize_llm_config(config)
199 super().__init__(llm_config, prompt_builder=prompt_builder)
201 async def initialize(self) -> None:
202 """Initialize Anthropic client."""
203 try:
204 import anthropic
206 api_key = self.config.api_key or os.environ.get('ANTHROPIC_API_KEY')
207 if not api_key:
208 raise ValueError("Anthropic API key not provided")
210 self._client = anthropic.AsyncAnthropic(
211 api_key=api_key,
212 base_url=self.config.api_base,
213 timeout=self.config.timeout
214 )
215 self._is_initialized = True
216 except ImportError as e:
217 raise ImportError("anthropic package not installed. Install with: pip install anthropic") from e
219 async def close(self) -> None:
220 """Close Anthropic client."""
221 if self._client:
222 await self._client.close() # type: ignore[unreachable]
223 self._is_initialized = False
225 async def validate_model(self) -> bool:
226 """Validate model availability."""
227 valid_models = [
228 'claude-3-opus', 'claude-3-sonnet', 'claude-3-haiku',
229 'claude-2.1', 'claude-2.0', 'claude-instant-1.2'
230 ]
231 return any(m in self.config.model for m in valid_models)
233 def get_capabilities(self) -> List[ModelCapability]:
234 """Get Anthropic model capabilities."""
235 capabilities = [
236 ModelCapability.TEXT_GENERATION,
237 ModelCapability.CHAT,
238 ModelCapability.STREAMING,
239 ModelCapability.CODE
240 ]
242 # Claude 3+ models support vision and tools
243 if 'claude-3' in self.config.model or 'claude-sonnet' in self.config.model or 'claude-opus' in self.config.model:
244 capabilities.extend([
245 ModelCapability.VISION,
246 ModelCapability.FUNCTION_CALLING
247 ])
249 return capabilities
251 async def complete(
252 self,
253 messages: Union[str, List[LLMMessage]],
254 config_overrides: Dict[str, Any] | None = None,
255 **kwargs
256 ) -> LLMResponse:
257 """Generate completion.
259 Args:
260 messages: Input messages or prompt
261 config_overrides: Optional dict to override config fields (model,
262 temperature, max_tokens, top_p, stop_sequences, seed)
263 **kwargs: Additional provider-specific parameters
264 """
265 if not self._is_initialized:
266 await self.initialize()
268 # Get runtime config (with overrides applied if provided)
269 runtime_config = self._get_runtime_config(config_overrides)
271 # Convert to Anthropic format
272 if isinstance(messages, str):
273 prompt = messages
274 else:
275 # Build prompt from messages
276 prompt = ""
277 for msg in messages:
278 if msg.role == 'system':
279 prompt = msg.content + "\n\n" + prompt
280 elif msg.role == 'user':
281 prompt += f"\n\nHuman: {msg.content}"
282 elif msg.role == 'assistant':
283 prompt += f"\n\nAssistant: {msg.content}"
284 prompt += "\n\nAssistant:"
286 # Make API call
287 response = await self._client.messages.create(
288 model=runtime_config.model,
289 messages=[{"role": "user", "content": prompt}],
290 max_tokens=runtime_config.max_tokens or 1024,
291 temperature=runtime_config.temperature,
292 top_p=runtime_config.top_p,
293 stop_sequences=runtime_config.stop_sequences
294 )
296 return LLMResponse(
297 content=response.content[0].text,
298 model=response.model,
299 finish_reason=response.stop_reason,
300 usage={
301 'prompt_tokens': response.usage.input_tokens,
302 'completion_tokens': response.usage.output_tokens,
303 'total_tokens': response.usage.input_tokens + response.usage.output_tokens
304 } if hasattr(response, 'usage') else None
305 )
307 async def stream_complete(
308 self,
309 messages: Union[str, List[LLMMessage]],
310 config_overrides: Dict[str, Any] | None = None,
311 **kwargs
312 ) -> AsyncIterator[LLMStreamResponse]:
313 """Generate streaming completion.
315 Args:
316 messages: Input messages or prompt
317 config_overrides: Optional dict to override config fields (model,
318 temperature, max_tokens, top_p, stop_sequences, seed)
319 **kwargs: Additional provider-specific parameters
320 """
321 if not self._is_initialized:
322 await self.initialize()
324 # Get runtime config (with overrides applied if provided)
325 runtime_config = self._get_runtime_config(config_overrides)
327 # Convert to Anthropic format
328 if isinstance(messages, str):
329 prompt = messages
330 else:
331 prompt = self._build_prompt(messages)
333 # Stream API call
334 async with self._client.messages.stream(
335 model=runtime_config.model,
336 messages=[{"role": "user", "content": prompt}],
337 max_tokens=runtime_config.max_tokens or 1024,
338 temperature=runtime_config.temperature
339 ) as stream:
340 async for chunk in stream:
341 if chunk.type == 'content_block_delta':
342 yield LLMStreamResponse(
343 delta=chunk.delta.text,
344 is_final=False
345 )
347 # Final message
348 message = await stream.get_final_message()
349 yield LLMStreamResponse(
350 delta='',
351 is_final=True,
352 finish_reason=message.stop_reason
353 )
355 async def embed(
356 self,
357 texts: Union[str, List[str]],
358 **kwargs
359 ) -> Union[List[float], List[List[float]]]:
360 """Anthropic doesn't provide embeddings."""
361 raise NotImplementedError("Anthropic doesn't provide embedding models")
363 async def function_call(
364 self,
365 messages: List[LLMMessage],
366 functions: List[Dict[str, Any]],
367 **kwargs
368 ) -> LLMResponse:
369 """Execute function calling with native Anthropic tools API (Claude 3+)."""
370 if not self._is_initialized:
371 await self.initialize()
373 # Convert to Anthropic message format
374 anthropic_messages = []
375 system_content = self.config.system_prompt or ''
377 for msg in messages:
378 if msg.role == 'system':
379 # Anthropic uses system parameter, not system messages
380 system_content = msg.content if not system_content else f"{system_content}\n\n{msg.content}"
381 else:
382 anthropic_messages.append({
383 'role': msg.role,
384 'content': msg.content
385 })
387 # Convert functions to Anthropic tools format
388 tools = []
389 for func in functions:
390 tool = {
391 'name': func.get('name', ''),
392 'description': func.get('description', ''),
393 'input_schema': func.get('parameters', {
394 'type': 'object',
395 'properties': {},
396 'required': []
397 })
398 }
399 tools.append(tool)
401 # Make API call with tools
402 try:
403 response = await self._client.messages.create(
404 model=self.config.model,
405 messages=anthropic_messages,
406 system=system_content if system_content else None,
407 tools=tools,
408 max_tokens=self.config.max_tokens or 1024,
409 temperature=self.config.temperature,
410 top_p=self.config.top_p
411 )
413 # Extract response content and tool use
414 content = ''
415 tool_use = None
417 for block in response.content:
418 if block.type == 'text':
419 content += block.text
420 elif block.type == 'tool_use':
421 tool_use = {
422 'name': block.name,
423 'arguments': block.input
424 }
426 llm_response = LLMResponse(
427 content=content,
428 model=response.model,
429 finish_reason=response.stop_reason,
430 usage={
431 'prompt_tokens': response.usage.input_tokens,
432 'completion_tokens': response.usage.output_tokens,
433 'total_tokens': response.usage.input_tokens + response.usage.output_tokens
434 },
435 function_call=tool_use
436 )
438 return llm_response
440 except Exception as e:
441 # Fallback to prompt-based approach for older models
442 import logging
443 logging.warning(f"Anthropic native tools failed, falling back to prompt-based: {e}")
445 function_descriptions = "\n".join([
446 f"- {f['name']}: {f['description']}"
447 for f in functions
448 ])
450 system_prompt = f"""You have access to the following functions:
451{function_descriptions}
453When you need to call a function, respond with:
454FUNCTION_CALL: {{
455 "name": "function_name",
456 "arguments": {{...}}
457}}"""
459 messages_with_system = [
460 LLMMessage(role='system', content=system_prompt)
461 ] + list(messages)
463 response = await self.complete(messages_with_system, **kwargs)
465 # Parse function call from response
466 if 'FUNCTION_CALL:' in response.content:
467 try:
468 func_json = response.content.split('FUNCTION_CALL:')[1].strip()
469 function_call = json.loads(func_json)
470 response.function_call = function_call
471 except (json.JSONDecodeError, IndexError):
472 pass
474 return response
476 def _build_prompt(self, messages: List[LLMMessage]) -> str:
477 """Build Anthropic-style prompt from messages."""
478 prompt = ""
479 for msg in messages:
480 if msg.role == 'system':
481 prompt = msg.content + "\n\n" + prompt
482 elif msg.role == 'user':
483 prompt += f"\n\nHuman: {msg.content}"
484 elif msg.role == 'assistant':
485 prompt += f"\n\nAssistant: {msg.content}"
486 prompt += "\n\nAssistant:"
487 return prompt