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.
@@ -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.1.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
- def list_models() -> None:
154
- """List supported LLM models.
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
- These are model identifiers that work with our direct API clients.
157
- Only models with direct API support are listed.
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
- models = [
160
- (
161
- "Groq (Fast, Free Tier Available)",
162
- [
163
- "groq/llama-3.1-8b-instant",
164
- "groq/llama-3.1-70b-versatile",
165
- "groq/llama-3.2-1b-preview",
166
- "groq/llama-3.2-3b-preview",
167
- "groq/mixtral-8x7b-32768",
168
- "groq/gemma-7b-it",
169
- "groq/gemma2-9b-it",
170
- ],
171
- ),
172
- (
173
- "Google Gemini (Free Tier Available)",
174
- [
175
- "gemini/gemini-2.5-flash",
176
- "gemini/gemini-1.5-pro",
177
- "gemini/gemini-1.5-flash",
178
- "gemini/gemini-1.5-flash-8b",
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
- click.echo("\nSupported LLM Models (Direct API Access):\n")
184
- for provider, model_list in models:
185
- click.echo(f" {provider}:")
186
- for m in model_list:
187
- click.echo(f" - {m}")
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
- " GEMINI_API_KEY - Get free API key at https://aistudio.google.com"
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
- click.echo("Default model: groq/llama-3.1-8b-instant")
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.gemini_client import (
4
- GeminiClient,
5
- GeminiConfig,
6
- GeminiResponse,
3
+ from causaliq_knowledge.llm.anthropic_client import (
4
+ AnthropicClient,
5
+ AnthropicConfig,
7
6
  )
8
- from causaliq_knowledge.llm.groq_client import (
9
- GroqClient,
10
- GroqConfig,
11
- GroqResponse,
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
- "GeminiResponse",
46
+ # Groq
27
47
  "GroqClient",
28
48
  "GroqConfig",
29
- "GroqResponse",
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 []