causaliq-knowledge 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- causaliq_knowledge/__init__.py +1 -1
- causaliq_knowledge/cli.py +244 -37
- causaliq_knowledge/llm/__init__.py +39 -10
- causaliq_knowledge/llm/anthropic_client.py +256 -0
- causaliq_knowledge/llm/base_client.py +220 -0
- causaliq_knowledge/llm/deepseek_client.py +108 -0
- causaliq_knowledge/llm/gemini_client.py +117 -39
- causaliq_knowledge/llm/groq_client.py +115 -40
- causaliq_knowledge/llm/mistral_client.py +122 -0
- causaliq_knowledge/llm/ollama_client.py +240 -0
- causaliq_knowledge/llm/openai_client.py +115 -0
- causaliq_knowledge/llm/openai_compat_client.py +287 -0
- causaliq_knowledge/llm/provider.py +99 -46
- {causaliq_knowledge-0.1.0.dist-info → causaliq_knowledge-0.2.0.dist-info}/METADATA +8 -9
- causaliq_knowledge-0.2.0.dist-info/RECORD +22 -0
- causaliq_knowledge-0.1.0.dist-info/RECORD +0 -15
- {causaliq_knowledge-0.1.0.dist-info → causaliq_knowledge-0.2.0.dist-info}/WHEEL +0 -0
- {causaliq_knowledge-0.1.0.dist-info → causaliq_knowledge-0.2.0.dist-info}/entry_points.txt +0 -0
- {causaliq_knowledge-0.1.0.dist-info → causaliq_knowledge-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {causaliq_knowledge-0.1.0.dist-info → causaliq_knowledge-0.2.0.dist-info}/top_level.txt +0 -0
causaliq_knowledge/__init__.py
CHANGED
|
@@ -5,7 +5,7 @@ causaliq-knowledge: LLM and human knowledge for causal discovery.
|
|
|
5
5
|
from causaliq_knowledge.base import KnowledgeProvider
|
|
6
6
|
from causaliq_knowledge.models import EdgeDirection, EdgeKnowledge
|
|
7
7
|
|
|
8
|
-
__version__ = "0.
|
|
8
|
+
__version__ = "0.2.0"
|
|
9
9
|
__author__ = "CausalIQ"
|
|
10
10
|
__email__ = "info@causaliq.com"
|
|
11
11
|
|
causaliq_knowledge/cli.py
CHANGED
|
@@ -150,51 +150,258 @@ def query_edge(
|
|
|
150
150
|
|
|
151
151
|
|
|
152
152
|
@cli.command("models")
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
@click.argument("provider", required=False, default=None)
|
|
154
|
+
def list_models(provider: Optional[str]) -> None:
|
|
155
|
+
"""List available LLM models from each provider.
|
|
155
156
|
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
Queries each provider's API to show models accessible with your
|
|
158
|
+
current configuration. Results are filtered by your API key's
|
|
159
|
+
access level or locally installed models.
|
|
160
|
+
|
|
161
|
+
Optionally specify PROVIDER to list models from a single provider:
|
|
162
|
+
groq, anthropic, gemini, ollama, openai, deepseek, or mistral.
|
|
163
|
+
|
|
164
|
+
Examples:
|
|
165
|
+
|
|
166
|
+
cqknow models # List all providers
|
|
167
|
+
|
|
168
|
+
cqknow models groq # List only Groq models
|
|
169
|
+
|
|
170
|
+
cqknow models mistral # List only Mistral models
|
|
158
171
|
"""
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
172
|
+
from typing import Callable, List, Optional, Tuple, TypedDict
|
|
173
|
+
|
|
174
|
+
from causaliq_knowledge.llm import (
|
|
175
|
+
AnthropicClient,
|
|
176
|
+
AnthropicConfig,
|
|
177
|
+
DeepSeekClient,
|
|
178
|
+
DeepSeekConfig,
|
|
179
|
+
GeminiClient,
|
|
180
|
+
GeminiConfig,
|
|
181
|
+
GroqClient,
|
|
182
|
+
GroqConfig,
|
|
183
|
+
MistralClient,
|
|
184
|
+
MistralConfig,
|
|
185
|
+
OllamaClient,
|
|
186
|
+
OllamaConfig,
|
|
187
|
+
OpenAIClient,
|
|
188
|
+
OpenAIConfig,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Type for get_models functions
|
|
192
|
+
GetModelsFunc = Callable[[], Tuple[bool, List[str], Optional[str]]]
|
|
193
|
+
|
|
194
|
+
class ProviderInfo(TypedDict):
|
|
195
|
+
name: str
|
|
196
|
+
prefix: str
|
|
197
|
+
env_var: Optional[str]
|
|
198
|
+
url: str
|
|
199
|
+
get_models: GetModelsFunc
|
|
200
|
+
|
|
201
|
+
def get_groq_models() -> Tuple[bool, List[str], Optional[str]]:
|
|
202
|
+
"""Returns (available, models, error_msg)."""
|
|
203
|
+
try:
|
|
204
|
+
client = GroqClient(GroqConfig())
|
|
205
|
+
if not client.is_available():
|
|
206
|
+
return False, [], "GROQ_API_KEY not set"
|
|
207
|
+
models = [f"groq/{m}" for m in client.list_models()]
|
|
208
|
+
return True, models, None
|
|
209
|
+
except ValueError as e:
|
|
210
|
+
return False, [], str(e)
|
|
211
|
+
|
|
212
|
+
def get_anthropic_models() -> Tuple[bool, List[str], Optional[str]]:
|
|
213
|
+
"""Returns (available, models, error_msg)."""
|
|
214
|
+
try:
|
|
215
|
+
client = AnthropicClient(AnthropicConfig())
|
|
216
|
+
if not client.is_available():
|
|
217
|
+
return False, [], "ANTHROPIC_API_KEY not set"
|
|
218
|
+
models = [f"anthropic/{m}" for m in client.list_models()]
|
|
219
|
+
return True, models, None
|
|
220
|
+
except ValueError as e:
|
|
221
|
+
return False, [], str(e)
|
|
222
|
+
|
|
223
|
+
def get_gemini_models() -> Tuple[bool, List[str], Optional[str]]:
|
|
224
|
+
"""Returns (available, models, error_msg)."""
|
|
225
|
+
try:
|
|
226
|
+
client = GeminiClient(GeminiConfig())
|
|
227
|
+
if not client.is_available():
|
|
228
|
+
return False, [], "GEMINI_API_KEY not set"
|
|
229
|
+
models = [f"gemini/{m}" for m in client.list_models()]
|
|
230
|
+
return True, models, None
|
|
231
|
+
except ValueError as e:
|
|
232
|
+
return False, [], str(e)
|
|
233
|
+
|
|
234
|
+
def get_ollama_models() -> Tuple[bool, List[str], Optional[str]]:
|
|
235
|
+
"""Returns (available, models, error_msg)."""
|
|
236
|
+
try:
|
|
237
|
+
client = OllamaClient(OllamaConfig())
|
|
238
|
+
models = [f"ollama/{m}" for m in client.list_models()]
|
|
239
|
+
if not models:
|
|
240
|
+
msg = "No models installed. Run: ollama pull <model>"
|
|
241
|
+
return True, [], msg
|
|
242
|
+
return True, models, None
|
|
243
|
+
except ValueError as e:
|
|
244
|
+
return False, [], str(e)
|
|
245
|
+
|
|
246
|
+
def get_openai_models() -> Tuple[bool, List[str], Optional[str]]:
|
|
247
|
+
"""Returns (available, models, error_msg)."""
|
|
248
|
+
try:
|
|
249
|
+
client = OpenAIClient(OpenAIConfig())
|
|
250
|
+
if not client.is_available():
|
|
251
|
+
return False, [], "OPENAI_API_KEY not set"
|
|
252
|
+
models = [f"openai/{m}" for m in client.list_models()]
|
|
253
|
+
return True, models, None
|
|
254
|
+
except ValueError as e:
|
|
255
|
+
return False, [], str(e)
|
|
256
|
+
|
|
257
|
+
def get_deepseek_models() -> Tuple[bool, List[str], Optional[str]]:
|
|
258
|
+
"""Returns (available, models, error_msg)."""
|
|
259
|
+
try:
|
|
260
|
+
client = DeepSeekClient(DeepSeekConfig())
|
|
261
|
+
if not client.is_available():
|
|
262
|
+
return False, [], "DEEPSEEK_API_KEY not set"
|
|
263
|
+
models = [f"deepseek/{m}" for m in client.list_models()]
|
|
264
|
+
return True, models, None
|
|
265
|
+
except ValueError as e:
|
|
266
|
+
return False, [], str(e)
|
|
267
|
+
|
|
268
|
+
def get_mistral_models() -> Tuple[bool, List[str], Optional[str]]:
|
|
269
|
+
"""Returns (available, models, error_msg)."""
|
|
270
|
+
try:
|
|
271
|
+
client = MistralClient(MistralConfig())
|
|
272
|
+
if not client.is_available():
|
|
273
|
+
return False, [], "MISTRAL_API_KEY not set"
|
|
274
|
+
models = [f"mistral/{m}" for m in client.list_models()]
|
|
275
|
+
return True, models, None
|
|
276
|
+
except ValueError as e:
|
|
277
|
+
return False, [], str(e)
|
|
278
|
+
|
|
279
|
+
providers: List[ProviderInfo] = [
|
|
280
|
+
{
|
|
281
|
+
"name": "Groq",
|
|
282
|
+
"prefix": "groq/",
|
|
283
|
+
"env_var": "GROQ_API_KEY",
|
|
284
|
+
"url": "https://console.groq.com",
|
|
285
|
+
"get_models": get_groq_models,
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
"name": "Anthropic",
|
|
289
|
+
"prefix": "anthropic/",
|
|
290
|
+
"env_var": "ANTHROPIC_API_KEY",
|
|
291
|
+
"url": "https://console.anthropic.com",
|
|
292
|
+
"get_models": get_anthropic_models,
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
"name": "Gemini",
|
|
296
|
+
"prefix": "gemini/",
|
|
297
|
+
"env_var": "GEMINI_API_KEY",
|
|
298
|
+
"url": "https://aistudio.google.com",
|
|
299
|
+
"get_models": get_gemini_models,
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
"name": "Ollama (Local)",
|
|
303
|
+
"prefix": "ollama/",
|
|
304
|
+
"env_var": None,
|
|
305
|
+
"url": "https://ollama.ai",
|
|
306
|
+
"get_models": get_ollama_models,
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
"name": "OpenAI",
|
|
310
|
+
"prefix": "openai/",
|
|
311
|
+
"env_var": "OPENAI_API_KEY",
|
|
312
|
+
"url": "https://platform.openai.com",
|
|
313
|
+
"get_models": get_openai_models,
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"name": "DeepSeek",
|
|
317
|
+
"prefix": "deepseek/",
|
|
318
|
+
"env_var": "DEEPSEEK_API_KEY",
|
|
319
|
+
"url": "https://platform.deepseek.com",
|
|
320
|
+
"get_models": get_deepseek_models,
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
"name": "Mistral",
|
|
324
|
+
"prefix": "mistral/",
|
|
325
|
+
"env_var": "MISTRAL_API_KEY",
|
|
326
|
+
"url": "https://console.mistral.ai",
|
|
327
|
+
"get_models": get_mistral_models,
|
|
328
|
+
},
|
|
181
329
|
]
|
|
182
330
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
331
|
+
# Filter providers if a specific one is requested
|
|
332
|
+
valid_provider_names = [
|
|
333
|
+
"groq",
|
|
334
|
+
"anthropic",
|
|
335
|
+
"gemini",
|
|
336
|
+
"ollama",
|
|
337
|
+
"openai",
|
|
338
|
+
"deepseek",
|
|
339
|
+
"mistral",
|
|
340
|
+
]
|
|
341
|
+
if provider:
|
|
342
|
+
provider_lower = provider.lower()
|
|
343
|
+
if provider_lower not in valid_provider_names:
|
|
344
|
+
click.echo(
|
|
345
|
+
f"Unknown provider: {provider}. "
|
|
346
|
+
f"Valid options: {', '.join(valid_provider_names)}",
|
|
347
|
+
err=True,
|
|
348
|
+
)
|
|
349
|
+
sys.exit(1)
|
|
350
|
+
providers = [
|
|
351
|
+
p for p in providers if p["prefix"].rstrip("/") == provider_lower
|
|
352
|
+
]
|
|
353
|
+
|
|
354
|
+
click.echo("\nAvailable LLM Models:\n")
|
|
355
|
+
|
|
356
|
+
any_available = False
|
|
357
|
+
for prov in providers:
|
|
358
|
+
available, models, error = prov["get_models"]()
|
|
359
|
+
|
|
360
|
+
if available and models:
|
|
361
|
+
any_available = True
|
|
362
|
+
status = click.style("[OK]", fg="green")
|
|
363
|
+
count = len(models)
|
|
364
|
+
click.echo(f" {status} {prov['name']} ({count} models):")
|
|
365
|
+
for m in models:
|
|
366
|
+
click.echo(f" {m}")
|
|
367
|
+
elif available and not models:
|
|
368
|
+
status = click.style("[!]", fg="yellow")
|
|
369
|
+
click.echo(f" {status} {prov['name']}:")
|
|
370
|
+
click.echo(f" {error}")
|
|
371
|
+
else:
|
|
372
|
+
status = click.style("[X]", fg="red")
|
|
373
|
+
click.echo(f" {status} {prov['name']}:")
|
|
374
|
+
click.echo(f" {error}")
|
|
375
|
+
|
|
376
|
+
click.echo()
|
|
377
|
+
|
|
378
|
+
click.echo("Provider Setup:")
|
|
379
|
+
for prov in providers:
|
|
380
|
+
available, _, _ = prov["get_models"]()
|
|
381
|
+
if prov["env_var"]:
|
|
382
|
+
status = "configured" if available else "not set"
|
|
383
|
+
color = "green" if available else "yellow"
|
|
384
|
+
click.echo(
|
|
385
|
+
f" {prov['env_var']}: "
|
|
386
|
+
f"{click.style(status, fg=color)} - {prov['url']}"
|
|
387
|
+
)
|
|
388
|
+
else:
|
|
389
|
+
status = "running" if available else "not running"
|
|
390
|
+
color = "green" if available else "yellow"
|
|
391
|
+
click.echo(
|
|
392
|
+
f" Ollama server: "
|
|
393
|
+
f"{click.style(status, fg=color)} - {prov['url']}"
|
|
394
|
+
)
|
|
395
|
+
|
|
188
396
|
click.echo()
|
|
189
|
-
click.echo("Required API Keys:")
|
|
190
|
-
click.echo(
|
|
191
|
-
" GROQ_API_KEY - Get free API key at https://console.groq.com"
|
|
192
|
-
)
|
|
193
397
|
click.echo(
|
|
194
|
-
"
|
|
398
|
+
click.style("Note: ", fg="yellow")
|
|
399
|
+
+ "Some models may require a paid plan. "
|
|
400
|
+
+ "Free tier availability varies by provider."
|
|
195
401
|
)
|
|
196
402
|
click.echo()
|
|
197
|
-
|
|
403
|
+
if any_available:
|
|
404
|
+
click.echo("Default model: groq/llama-3.1-8b-instant")
|
|
198
405
|
click.echo()
|
|
199
406
|
|
|
200
407
|
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
"""LLM integration module for causaliq-knowledge."""
|
|
2
2
|
|
|
3
|
-
from causaliq_knowledge.llm.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
GeminiResponse,
|
|
3
|
+
from causaliq_knowledge.llm.anthropic_client import (
|
|
4
|
+
AnthropicClient,
|
|
5
|
+
AnthropicConfig,
|
|
7
6
|
)
|
|
8
|
-
from causaliq_knowledge.llm.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
from causaliq_knowledge.llm.base_client import (
|
|
8
|
+
BaseLLMClient,
|
|
9
|
+
LLMConfig,
|
|
10
|
+
LLMResponse,
|
|
12
11
|
)
|
|
12
|
+
from causaliq_knowledge.llm.deepseek_client import (
|
|
13
|
+
DeepSeekClient,
|
|
14
|
+
DeepSeekConfig,
|
|
15
|
+
)
|
|
16
|
+
from causaliq_knowledge.llm.gemini_client import GeminiClient, GeminiConfig
|
|
17
|
+
from causaliq_knowledge.llm.groq_client import GroqClient, GroqConfig
|
|
18
|
+
from causaliq_knowledge.llm.mistral_client import MistralClient, MistralConfig
|
|
19
|
+
from causaliq_knowledge.llm.ollama_client import OllamaClient, OllamaConfig
|
|
20
|
+
from causaliq_knowledge.llm.openai_client import OpenAIClient, OpenAIConfig
|
|
13
21
|
from causaliq_knowledge.llm.prompts import EdgeQueryPrompt, parse_edge_response
|
|
14
22
|
from causaliq_knowledge.llm.provider import (
|
|
15
23
|
CONSENSUS_STRATEGIES,
|
|
@@ -19,14 +27,35 @@ from causaliq_knowledge.llm.provider import (
|
|
|
19
27
|
)
|
|
20
28
|
|
|
21
29
|
__all__ = [
|
|
30
|
+
# Abstract base
|
|
31
|
+
"BaseLLMClient",
|
|
32
|
+
"LLMConfig",
|
|
33
|
+
"LLMResponse",
|
|
34
|
+
# Anthropic
|
|
35
|
+
"AnthropicClient",
|
|
36
|
+
"AnthropicConfig",
|
|
37
|
+
# Consensus
|
|
22
38
|
"CONSENSUS_STRATEGIES",
|
|
39
|
+
# DeepSeek
|
|
40
|
+
"DeepSeekClient",
|
|
41
|
+
"DeepSeekConfig",
|
|
23
42
|
"EdgeQueryPrompt",
|
|
43
|
+
# Gemini
|
|
24
44
|
"GeminiClient",
|
|
25
45
|
"GeminiConfig",
|
|
26
|
-
|
|
46
|
+
# Groq
|
|
27
47
|
"GroqClient",
|
|
28
48
|
"GroqConfig",
|
|
29
|
-
|
|
49
|
+
# Mistral
|
|
50
|
+
"MistralClient",
|
|
51
|
+
"MistralConfig",
|
|
52
|
+
# Ollama (local)
|
|
53
|
+
"OllamaClient",
|
|
54
|
+
"OllamaConfig",
|
|
55
|
+
# OpenAI
|
|
56
|
+
"OpenAIClient",
|
|
57
|
+
"OpenAIConfig",
|
|
58
|
+
# Provider
|
|
30
59
|
"LLMKnowledge",
|
|
31
60
|
"highest_confidence",
|
|
32
61
|
"parse_edge_response",
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Direct Anthropic API client - clean and reliable."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from causaliq_knowledge.llm.base_client import (
|
|
11
|
+
BaseLLMClient,
|
|
12
|
+
LLMConfig,
|
|
13
|
+
LLMResponse,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class AnthropicConfig(LLMConfig):
|
|
21
|
+
"""Configuration for Anthropic API client.
|
|
22
|
+
|
|
23
|
+
Extends LLMConfig with Anthropic-specific defaults.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
model: Anthropic model identifier (default: claude-sonnet-4-20250514).
|
|
27
|
+
temperature: Sampling temperature (default: 0.1).
|
|
28
|
+
max_tokens: Maximum response tokens (default: 500).
|
|
29
|
+
timeout: Request timeout in seconds (default: 30.0).
|
|
30
|
+
api_key: Anthropic API key (falls back to ANTHROPIC_API_KEY env var).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
model: str = "claude-sonnet-4-20250514"
|
|
34
|
+
temperature: float = 0.1
|
|
35
|
+
max_tokens: int = 500
|
|
36
|
+
timeout: float = 30.0
|
|
37
|
+
api_key: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
def __post_init__(self) -> None:
|
|
40
|
+
"""Set API key from environment if not provided."""
|
|
41
|
+
if self.api_key is None:
|
|
42
|
+
self.api_key = os.getenv("ANTHROPIC_API_KEY")
|
|
43
|
+
if not self.api_key:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
"ANTHROPIC_API_KEY environment variable is required"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AnthropicClient(BaseLLMClient):
|
|
50
|
+
"""Direct Anthropic API client.
|
|
51
|
+
|
|
52
|
+
Implements the BaseLLMClient interface for Anthropic's Claude API.
|
|
53
|
+
Uses httpx for HTTP requests.
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
>>> config = AnthropicConfig(model="claude-sonnet-4-20250514")
|
|
57
|
+
>>> client = AnthropicClient(config)
|
|
58
|
+
>>> msgs = [{"role": "user", "content": "Hello"}]
|
|
59
|
+
>>> response = client.completion(msgs)
|
|
60
|
+
>>> print(response.content)
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
BASE_URL = "https://api.anthropic.com/v1"
|
|
64
|
+
API_VERSION = "2023-06-01"
|
|
65
|
+
|
|
66
|
+
def __init__(self, config: Optional[AnthropicConfig] = None) -> None:
|
|
67
|
+
"""Initialize Anthropic client.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
config: Anthropic configuration. If None, uses defaults with
|
|
71
|
+
API key from ANTHROPIC_API_KEY environment variable.
|
|
72
|
+
"""
|
|
73
|
+
self.config = config or AnthropicConfig()
|
|
74
|
+
self._total_calls = 0
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def provider_name(self) -> str:
|
|
78
|
+
"""Return the provider name."""
|
|
79
|
+
return "anthropic"
|
|
80
|
+
|
|
81
|
+
def completion(
|
|
82
|
+
self, messages: List[Dict[str, str]], **kwargs: Any
|
|
83
|
+
) -> LLMResponse:
|
|
84
|
+
"""Make a chat completion request to Anthropic.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
messages: List of message dicts with "role" and "content" keys.
|
|
88
|
+
**kwargs: Override config options (temperature, max_tokens).
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
LLMResponse with the generated content and metadata.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
ValueError: If the API request fails.
|
|
95
|
+
"""
|
|
96
|
+
# Anthropic uses separate system parameter, not in messages
|
|
97
|
+
system_content = None
|
|
98
|
+
filtered_messages = []
|
|
99
|
+
|
|
100
|
+
for msg in messages:
|
|
101
|
+
if msg["role"] == "system":
|
|
102
|
+
system_content = msg["content"]
|
|
103
|
+
else:
|
|
104
|
+
filtered_messages.append(msg)
|
|
105
|
+
|
|
106
|
+
# Build request payload in Anthropic's format
|
|
107
|
+
payload: Dict[str, Any] = {
|
|
108
|
+
"model": self.config.model,
|
|
109
|
+
"messages": filtered_messages,
|
|
110
|
+
"max_tokens": kwargs.get("max_tokens", self.config.max_tokens),
|
|
111
|
+
"temperature": kwargs.get("temperature", self.config.temperature),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Add system prompt if present
|
|
115
|
+
if system_content:
|
|
116
|
+
payload["system"] = system_content
|
|
117
|
+
|
|
118
|
+
# api_key is guaranteed non-None after __post_init__ validation
|
|
119
|
+
headers: dict[str, str] = {
|
|
120
|
+
"x-api-key": self.config.api_key, # type: ignore[dict-item]
|
|
121
|
+
"anthropic-version": self.API_VERSION,
|
|
122
|
+
"Content-Type": "application/json",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
logger.debug(f"Calling Anthropic API with model: {self.config.model}")
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
with httpx.Client(timeout=self.config.timeout) as client:
|
|
129
|
+
response = client.post(
|
|
130
|
+
f"{self.BASE_URL}/messages",
|
|
131
|
+
json=payload,
|
|
132
|
+
headers=headers,
|
|
133
|
+
)
|
|
134
|
+
response.raise_for_status()
|
|
135
|
+
|
|
136
|
+
data = response.json()
|
|
137
|
+
|
|
138
|
+
# Extract response content from Anthropic format
|
|
139
|
+
content_blocks = data.get("content", [])
|
|
140
|
+
content = ""
|
|
141
|
+
for block in content_blocks:
|
|
142
|
+
if block.get("type") == "text":
|
|
143
|
+
content += block.get("text", "")
|
|
144
|
+
|
|
145
|
+
# Extract usage info
|
|
146
|
+
usage = data.get("usage", {})
|
|
147
|
+
input_tokens = usage.get("input_tokens", 0)
|
|
148
|
+
output_tokens = usage.get("output_tokens", 0)
|
|
149
|
+
|
|
150
|
+
self._total_calls += 1
|
|
151
|
+
|
|
152
|
+
logger.debug(
|
|
153
|
+
f"Anthropic response: {input_tokens} in, "
|
|
154
|
+
f"{output_tokens} out"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return LLMResponse(
|
|
158
|
+
content=content,
|
|
159
|
+
model=data.get("model", self.config.model),
|
|
160
|
+
input_tokens=input_tokens,
|
|
161
|
+
output_tokens=output_tokens,
|
|
162
|
+
cost=0.0, # Cost calculation not implemented
|
|
163
|
+
raw_response=data,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
except httpx.HTTPStatusError as e:
|
|
167
|
+
try:
|
|
168
|
+
error_data = e.response.json()
|
|
169
|
+
error_msg = error_data.get("error", {}).get(
|
|
170
|
+
"message", e.response.text
|
|
171
|
+
)
|
|
172
|
+
except Exception:
|
|
173
|
+
error_msg = e.response.text
|
|
174
|
+
|
|
175
|
+
logger.error(
|
|
176
|
+
f"Anthropic API HTTP error: {e.response.status_code} - "
|
|
177
|
+
f"{error_msg}"
|
|
178
|
+
)
|
|
179
|
+
raise ValueError(
|
|
180
|
+
f"Anthropic API error: {e.response.status_code} - {error_msg}"
|
|
181
|
+
)
|
|
182
|
+
except httpx.TimeoutException:
|
|
183
|
+
raise ValueError("Anthropic API request timed out")
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logger.error(f"Anthropic API unexpected error: {e}")
|
|
186
|
+
raise ValueError(f"Anthropic API error: {str(e)}")
|
|
187
|
+
|
|
188
|
+
def complete_json(
|
|
189
|
+
self, messages: List[Dict[str, str]], **kwargs: Any
|
|
190
|
+
) -> tuple[Optional[Dict[str, Any]], LLMResponse]:
|
|
191
|
+
"""Make a completion request and parse response as JSON.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
messages: List of message dicts with "role" and "content" keys.
|
|
195
|
+
**kwargs: Override config options passed to completion().
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Tuple of (parsed JSON dict or None, raw LLMResponse).
|
|
199
|
+
"""
|
|
200
|
+
response = self.completion(messages, **kwargs)
|
|
201
|
+
parsed = response.parse_json()
|
|
202
|
+
return parsed, response
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def call_count(self) -> int:
|
|
206
|
+
"""Return the number of API calls made."""
|
|
207
|
+
return self._total_calls
|
|
208
|
+
|
|
209
|
+
def is_available(self) -> bool:
|
|
210
|
+
"""Check if Anthropic API is available.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
True if ANTHROPIC_API_KEY is configured.
|
|
214
|
+
"""
|
|
215
|
+
return bool(self.config.api_key)
|
|
216
|
+
|
|
217
|
+
def list_models(self) -> List[str]:
|
|
218
|
+
"""List available Claude models from Anthropic API.
|
|
219
|
+
|
|
220
|
+
Queries the Anthropic /v1/models endpoint to get available models.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
List of model identifiers
|
|
224
|
+
(e.g., ['claude-sonnet-4-20250514', ...]).
|
|
225
|
+
"""
|
|
226
|
+
if not self.config.api_key:
|
|
227
|
+
return []
|
|
228
|
+
|
|
229
|
+
headers: dict[str, str] = {
|
|
230
|
+
"x-api-key": self.config.api_key,
|
|
231
|
+
"anthropic-version": self.API_VERSION,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
with httpx.Client(timeout=self.config.timeout) as client:
|
|
236
|
+
response = client.get(
|
|
237
|
+
f"{self.BASE_URL}/models",
|
|
238
|
+
headers=headers,
|
|
239
|
+
)
|
|
240
|
+
response.raise_for_status()
|
|
241
|
+
|
|
242
|
+
data = response.json()
|
|
243
|
+
models = []
|
|
244
|
+
for model_info in data.get("data", []):
|
|
245
|
+
model_id = model_info.get("id")
|
|
246
|
+
if model_id:
|
|
247
|
+
models.append(model_id)
|
|
248
|
+
|
|
249
|
+
return sorted(models)
|
|
250
|
+
|
|
251
|
+
except httpx.HTTPStatusError as e:
|
|
252
|
+
logger.warning(f"Anthropic API error listing models: {e}")
|
|
253
|
+
return []
|
|
254
|
+
except Exception as e:
|
|
255
|
+
logger.warning(f"Error listing Anthropic models: {e}")
|
|
256
|
+
return []
|