jarviscore-framework 0.1.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.
Files changed (55) hide show
  1. examples/calculator_agent_example.py +77 -0
  2. examples/multi_agent_workflow.py +132 -0
  3. examples/research_agent_example.py +76 -0
  4. jarviscore/__init__.py +54 -0
  5. jarviscore/cli/__init__.py +7 -0
  6. jarviscore/cli/__main__.py +33 -0
  7. jarviscore/cli/check.py +404 -0
  8. jarviscore/cli/smoketest.py +371 -0
  9. jarviscore/config/__init__.py +7 -0
  10. jarviscore/config/settings.py +128 -0
  11. jarviscore/core/__init__.py +7 -0
  12. jarviscore/core/agent.py +163 -0
  13. jarviscore/core/mesh.py +463 -0
  14. jarviscore/core/profile.py +64 -0
  15. jarviscore/docs/API_REFERENCE.md +932 -0
  16. jarviscore/docs/CONFIGURATION.md +753 -0
  17. jarviscore/docs/GETTING_STARTED.md +600 -0
  18. jarviscore/docs/TROUBLESHOOTING.md +424 -0
  19. jarviscore/docs/USER_GUIDE.md +983 -0
  20. jarviscore/execution/__init__.py +94 -0
  21. jarviscore/execution/code_registry.py +298 -0
  22. jarviscore/execution/generator.py +268 -0
  23. jarviscore/execution/llm.py +430 -0
  24. jarviscore/execution/repair.py +283 -0
  25. jarviscore/execution/result_handler.py +332 -0
  26. jarviscore/execution/sandbox.py +555 -0
  27. jarviscore/execution/search.py +281 -0
  28. jarviscore/orchestration/__init__.py +18 -0
  29. jarviscore/orchestration/claimer.py +101 -0
  30. jarviscore/orchestration/dependency.py +143 -0
  31. jarviscore/orchestration/engine.py +292 -0
  32. jarviscore/orchestration/status.py +96 -0
  33. jarviscore/p2p/__init__.py +23 -0
  34. jarviscore/p2p/broadcaster.py +353 -0
  35. jarviscore/p2p/coordinator.py +364 -0
  36. jarviscore/p2p/keepalive.py +361 -0
  37. jarviscore/p2p/swim_manager.py +290 -0
  38. jarviscore/profiles/__init__.py +6 -0
  39. jarviscore/profiles/autoagent.py +264 -0
  40. jarviscore/profiles/customagent.py +137 -0
  41. jarviscore_framework-0.1.0.dist-info/METADATA +136 -0
  42. jarviscore_framework-0.1.0.dist-info/RECORD +55 -0
  43. jarviscore_framework-0.1.0.dist-info/WHEEL +5 -0
  44. jarviscore_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
  45. jarviscore_framework-0.1.0.dist-info/top_level.txt +3 -0
  46. tests/conftest.py +44 -0
  47. tests/test_agent.py +165 -0
  48. tests/test_autoagent.py +140 -0
  49. tests/test_autoagent_day4.py +186 -0
  50. tests/test_customagent.py +248 -0
  51. tests/test_integration.py +293 -0
  52. tests/test_llm_fallback.py +185 -0
  53. tests/test_mesh.py +356 -0
  54. tests/test_p2p_integration.py +375 -0
  55. tests/test_remote_sandbox.py +116 -0
@@ -0,0 +1,430 @@
1
+ """
2
+ Unified LLM Client - All providers in one file with zero-config setup
3
+ Supports: vLLM, Azure OpenAI, Gemini, Claude with automatic fallback
4
+ """
5
+ import asyncio
6
+ import aiohttp
7
+ import logging
8
+ import time
9
+ from typing import Optional, Dict, List, Any
10
+ from enum import Enum
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # Try importing optional LLM SDKs
15
+ try:
16
+ import google.generativeai as genai
17
+ GEMINI_AVAILABLE = True
18
+ except ImportError:
19
+ GEMINI_AVAILABLE = False
20
+ logger.debug("Gemini SDK not available (pip install google-generativeai)")
21
+
22
+ try:
23
+ from openai import AsyncAzureOpenAI
24
+ AZURE_AVAILABLE = True
25
+ except ImportError:
26
+ AZURE_AVAILABLE = False
27
+ logger.debug("Azure OpenAI SDK not available (pip install openai)")
28
+
29
+ try:
30
+ from anthropic import Anthropic
31
+ CLAUDE_AVAILABLE = True
32
+ except ImportError:
33
+ CLAUDE_AVAILABLE = False
34
+ logger.debug("Claude SDK not available (pip install anthropic)")
35
+
36
+
37
+ class LLMProvider(Enum):
38
+ """Available LLM providers."""
39
+ VLLM = "vllm"
40
+ AZURE = "azure"
41
+ GEMINI = "gemini"
42
+ CLAUDE = "claude"
43
+
44
+
45
+ # Token pricing per 1M tokens (updated 2025)
46
+ TOKEN_PRICING = {
47
+ "gpt-4o": {"input": 3.00, "output": 15.00, "cached": 1.50},
48
+ "gpt-4": {"input": 30.00, "output": 60.00, "cached": 15.00},
49
+ "gpt-3.5-turbo": {"input": 0.50, "output": 1.50, "cached": 0.25},
50
+ "gemini-1.5-pro": {"input": 1.25, "output": 5.00, "cached": 0.31},
51
+ "gemini-1.5-flash": {"input": 0.10, "output": 0.30, "cached": 0.03},
52
+ "claude-opus-4": {"input": 15.00, "output": 75.00, "cached": 3.75},
53
+ "claude-sonnet-4": {"input": 3.00, "output": 15.00, "cached": 0.75},
54
+ "claude-haiku-3.5": {"input": 1.00, "output": 5.00, "cached": 0.25},
55
+ }
56
+
57
+
58
+ class UnifiedLLMClient:
59
+ """
60
+ Zero-config LLM client with automatic provider detection and fallback.
61
+
62
+ Philosophy: Developer writes NOTHING. Framework tries providers in order:
63
+ 1. vLLM (local, free)
64
+ 2. Azure OpenAI (if configured)
65
+ 3. Gemini (if configured)
66
+ 4. Claude (if configured)
67
+
68
+ Example:
69
+ client = UnifiedLLMClient()
70
+ response = await client.generate("Write Python code to add 2+2")
71
+ # Framework automatically picks best available provider
72
+ """
73
+
74
+ def __init__(self, config: Optional[Dict] = None):
75
+ """
76
+ Initialize LLM client with zero-config defaults.
77
+
78
+ Args:
79
+ config: Optional config dict. If None, auto-detects from environment via Pydantic.
80
+ """
81
+ # Load from Pydantic settings first
82
+ from jarviscore.config import settings
83
+
84
+ # Merge: Pydantic settings as base, config dict as override
85
+ self.config = settings.model_dump()
86
+ if config:
87
+ self.config.update(config)
88
+
89
+ # Provider clients
90
+ self.vllm_endpoint = None
91
+ self.azure_client = None
92
+ self.gemini_client = None
93
+ self.claude_client = None
94
+
95
+ # Provider order (tries in this sequence)
96
+ self.provider_order = []
97
+
98
+ # Initialize all available providers
99
+ self._setup_providers()
100
+
101
+ logger.info(f"LLM Client initialized with providers: {[p.value for p in self.provider_order]}")
102
+
103
+ def _setup_providers(self):
104
+ """Auto-detect and setup available LLM providers."""
105
+
106
+ # 1. Try Claude first (primary provider)
107
+ if CLAUDE_AVAILABLE:
108
+ claude_key = self.config.get('claude_api_key') or self.config.get('anthropic_api_key')
109
+ claude_endpoint = self.config.get('claude_endpoint')
110
+ if claude_key:
111
+ try:
112
+ # Support custom Claude endpoint (e.g., Azure-hosted Claude)
113
+ if claude_endpoint:
114
+ self.claude_client = Anthropic(
115
+ api_key=claude_key,
116
+ base_url=claude_endpoint
117
+ )
118
+ logger.info(f"✓ Claude provider available with custom endpoint: {claude_endpoint}")
119
+ else:
120
+ self.claude_client = Anthropic(api_key=claude_key)
121
+ logger.info("✓ Claude provider available")
122
+ self.provider_order.append(LLMProvider.CLAUDE)
123
+ except Exception as e:
124
+ logger.warning(f"Failed to setup Claude: {e}")
125
+
126
+ # 2. Try vLLM (local, free)
127
+ vllm_endpoint = self.config.get('llm_endpoint') or self.config.get('vllm_endpoint')
128
+ if vllm_endpoint:
129
+ self.vllm_endpoint = vllm_endpoint.rstrip('/')
130
+ self.provider_order.append(LLMProvider.VLLM)
131
+ logger.info(f"✓ vLLM provider available: {self.vllm_endpoint}")
132
+
133
+ # 3. Try Azure OpenAI
134
+ if AZURE_AVAILABLE:
135
+ azure_key = self.config.get('azure_api_key') or self.config.get('azure_openai_key')
136
+ azure_endpoint = self.config.get('azure_endpoint') or self.config.get('azure_openai_endpoint')
137
+
138
+ if azure_key and azure_endpoint:
139
+ try:
140
+ self.azure_client = AsyncAzureOpenAI(
141
+ api_key=azure_key,
142
+ azure_endpoint=azure_endpoint,
143
+ api_version=self.config.get('azure_api_version', '2024-02-15-preview'),
144
+ timeout=self.config.get('llm_timeout', 120)
145
+ )
146
+ self.provider_order.append(LLMProvider.AZURE)
147
+ logger.info("✓ Azure OpenAI provider available")
148
+ except Exception as e:
149
+ logger.warning(f"Failed to setup Azure OpenAI: {e}")
150
+
151
+ # 4. Try Gemini
152
+ if GEMINI_AVAILABLE:
153
+ gemini_key = self.config.get('gemini_api_key')
154
+ if gemini_key:
155
+ try:
156
+ genai.configure(api_key=gemini_key)
157
+ model_name = self.config.get('gemini_model', 'gemini-1.5-flash')
158
+ self.gemini_client = genai.GenerativeModel(model_name)
159
+ self.provider_order.append(LLMProvider.GEMINI)
160
+ logger.info(f"✓ Gemini provider available: {model_name}")
161
+ except Exception as e:
162
+ logger.warning(f"Failed to setup Gemini: {e}")
163
+
164
+ if not self.provider_order:
165
+ logger.warning(
166
+ "⚠️ No LLM providers configured! Set at least one:\n"
167
+ " - llm_endpoint for vLLM\n"
168
+ " - azure_api_key + azure_endpoint for Azure\n"
169
+ " - gemini_api_key for Gemini\n"
170
+ " - claude_api_key for Claude"
171
+ )
172
+
173
+ async def generate(
174
+ self,
175
+ prompt: str,
176
+ messages: Optional[List[Dict]] = None,
177
+ temperature: float = 0.7,
178
+ max_tokens: int = 4000,
179
+ **kwargs
180
+ ) -> Dict[str, Any]:
181
+ """
182
+ Generate completion with automatic provider fallback.
183
+
184
+ Args:
185
+ prompt: Text prompt (if messages not provided)
186
+ messages: OpenAI-style message list [{"role": "user", "content": "..."}]
187
+ temperature: Sampling temperature (0-1)
188
+ max_tokens: Maximum tokens to generate
189
+ **kwargs: Additional provider-specific options
190
+
191
+ Returns:
192
+ {
193
+ "content": "generated text",
194
+ "provider": "vllm|azure|gemini|claude",
195
+ "tokens": {"input": 100, "output": 200, "total": 300},
196
+ "cost_usd": 0.015,
197
+ "model": "gpt-4o"
198
+ }
199
+ """
200
+ # Convert prompt to messages if needed
201
+ if not messages:
202
+ messages = [{"role": "user", "content": prompt}]
203
+
204
+ # Try each provider in order
205
+ last_error = None
206
+ for provider in self.provider_order:
207
+ try:
208
+ logger.debug(f"Trying provider: {provider.value}")
209
+
210
+ if provider == LLMProvider.VLLM:
211
+ return await self._call_vllm(messages, temperature, max_tokens, **kwargs)
212
+ elif provider == LLMProvider.AZURE:
213
+ return await self._call_azure(messages, temperature, max_tokens, **kwargs)
214
+ elif provider == LLMProvider.GEMINI:
215
+ return await self._call_gemini(messages, temperature, max_tokens, **kwargs)
216
+ elif provider == LLMProvider.CLAUDE:
217
+ return await self._call_claude(messages, temperature, max_tokens, **kwargs)
218
+
219
+ except Exception as e:
220
+ last_error = e
221
+ logger.warning(f"Provider {provider.value} failed: {e}")
222
+ continue
223
+
224
+ # All providers failed
225
+ raise RuntimeError(
226
+ f"All LLM providers failed. Last error: {last_error}\n"
227
+ f"Tried: {[p.value for p in self.provider_order]}"
228
+ )
229
+
230
+ async def _call_vllm(self, messages: List[Dict], temperature: float, max_tokens: int, **kwargs) -> Dict:
231
+ """Call vLLM endpoint."""
232
+ if not self.vllm_endpoint:
233
+ raise RuntimeError("vLLM endpoint not configured")
234
+
235
+ endpoint = self.vllm_endpoint
236
+ if not endpoint.endswith('/v1/chat/completions'):
237
+ endpoint = f"{endpoint}/v1/chat/completions"
238
+
239
+ payload = {
240
+ "model": self.config.get('llm_model', 'default'),
241
+ "messages": messages,
242
+ "temperature": temperature,
243
+ "max_tokens": max_tokens,
244
+ "stream": False
245
+ }
246
+
247
+ timeout = aiohttp.ClientTimeout(total=self.config.get('llm_timeout', 120))
248
+ start_time = time.time()
249
+
250
+ async with aiohttp.ClientSession(timeout=timeout) as session:
251
+ async with session.post(endpoint, json=payload) as response:
252
+ if response.status != 200:
253
+ error = await response.text()
254
+ raise RuntimeError(f"vLLM error {response.status}: {error}")
255
+
256
+ data = await response.json()
257
+ duration = time.time() - start_time
258
+
259
+ content = data['choices'][0]['message']['content']
260
+ usage = data.get('usage', {})
261
+
262
+ return {
263
+ "content": content,
264
+ "provider": "vllm",
265
+ "tokens": {
266
+ "input": usage.get('prompt_tokens', 0),
267
+ "output": usage.get('completion_tokens', 0),
268
+ "total": usage.get('total_tokens', 0)
269
+ },
270
+ "cost_usd": 0.0, # vLLM is free (local)
271
+ "model": payload['model'],
272
+ "duration_seconds": duration
273
+ }
274
+
275
+ async def _call_azure(self, messages: List[Dict], temperature: float, max_tokens: int, **kwargs) -> Dict:
276
+ """Call Azure OpenAI."""
277
+ if not self.azure_client:
278
+ raise RuntimeError("Azure client not initialized")
279
+
280
+ deployment = self.config.get('azure_deployment', 'gpt-4o')
281
+ start_time = time.time()
282
+
283
+ response = await self.azure_client.chat.completions.create(
284
+ model=deployment,
285
+ messages=messages,
286
+ temperature=temperature,
287
+ max_tokens=max_tokens
288
+ )
289
+
290
+ duration = time.time() - start_time
291
+ content = response.choices[0].message.content
292
+ usage = response.usage
293
+
294
+ # Calculate cost
295
+ pricing = TOKEN_PRICING.get(deployment, {"input": 3.0, "output": 15.0})
296
+ cost = (usage.prompt_tokens * pricing['input'] +
297
+ usage.completion_tokens * pricing['output']) / 1_000_000
298
+
299
+ return {
300
+ "content": content,
301
+ "provider": "azure",
302
+ "tokens": {
303
+ "input": usage.prompt_tokens,
304
+ "output": usage.completion_tokens,
305
+ "total": usage.total_tokens
306
+ },
307
+ "cost_usd": cost,
308
+ "model": deployment,
309
+ "duration_seconds": duration
310
+ }
311
+
312
+ async def _call_gemini(self, messages: List[Dict], temperature: float, max_tokens: int, **kwargs) -> Dict:
313
+ """Call Google Gemini."""
314
+ if not self.gemini_client:
315
+ raise RuntimeError("Gemini client not initialized")
316
+
317
+ # Convert messages to Gemini format
318
+ prompt = self._messages_to_prompt(messages)
319
+
320
+ start_time = time.time()
321
+ response = await asyncio.to_thread(
322
+ self.gemini_client.generate_content,
323
+ prompt,
324
+ generation_config={
325
+ "temperature": temperature,
326
+ "max_output_tokens": max_tokens
327
+ }
328
+ )
329
+ duration = time.time() - start_time
330
+
331
+ content = response.text
332
+
333
+ # Estimate tokens (Gemini doesn't always return usage)
334
+ input_tokens = len(prompt.split()) * 1.3 # rough estimate
335
+ output_tokens = len(content.split()) * 1.3
336
+
337
+ model_name = self.config.get('gemini_model', 'gemini-1.5-flash')
338
+ pricing = TOKEN_PRICING.get(model_name, {"input": 0.10, "output": 0.30})
339
+ cost = (input_tokens * pricing['input'] + output_tokens * pricing['output']) / 1_000_000
340
+
341
+ return {
342
+ "content": content,
343
+ "provider": "gemini",
344
+ "tokens": {
345
+ "input": int(input_tokens),
346
+ "output": int(output_tokens),
347
+ "total": int(input_tokens + output_tokens)
348
+ },
349
+ "cost_usd": cost,
350
+ "model": model_name,
351
+ "duration_seconds": duration
352
+ }
353
+
354
+ async def _call_claude(self, messages: List[Dict], temperature: float, max_tokens: int, **kwargs) -> Dict:
355
+ """Call Anthropic Claude."""
356
+ if not self.claude_client:
357
+ raise RuntimeError("Claude client not initialized")
358
+
359
+ # Separate system message from conversation
360
+ system_msg = None
361
+ conv_messages = []
362
+ for msg in messages:
363
+ if msg['role'] == 'system':
364
+ system_msg = msg['content']
365
+ else:
366
+ conv_messages.append(msg)
367
+
368
+ model = self.config.get('claude_model', 'claude-sonnet-4')
369
+ start_time = time.time()
370
+
371
+ # Prepare request kwargs
372
+ request_kwargs = {
373
+ "model": model,
374
+ "max_tokens": max_tokens,
375
+ "temperature": temperature,
376
+ "messages": conv_messages
377
+ }
378
+
379
+ # Only add system if it exists (Claude API requires it to be string or not present)
380
+ if system_msg:
381
+ request_kwargs["system"] = system_msg
382
+
383
+ response = await asyncio.to_thread(
384
+ self.claude_client.messages.create,
385
+ **request_kwargs
386
+ )
387
+
388
+ duration = time.time() - start_time
389
+ content = response.content[0].text
390
+
391
+ # Calculate cost
392
+ pricing = TOKEN_PRICING.get(model, {"input": 3.0, "output": 15.0})
393
+ cost = (response.usage.input_tokens * pricing['input'] +
394
+ response.usage.output_tokens * pricing['output']) / 1_000_000
395
+
396
+ return {
397
+ "content": content,
398
+ "provider": "claude",
399
+ "tokens": {
400
+ "input": response.usage.input_tokens,
401
+ "output": response.usage.output_tokens,
402
+ "total": response.usage.input_tokens + response.usage.output_tokens
403
+ },
404
+ "cost_usd": cost,
405
+ "model": model,
406
+ "duration_seconds": duration
407
+ }
408
+
409
+ def _messages_to_prompt(self, messages: List[Dict]) -> str:
410
+ """Convert OpenAI message format to plain text prompt."""
411
+ parts = []
412
+ for msg in messages:
413
+ role = msg['role']
414
+ content = msg['content']
415
+ if role == 'system':
416
+ parts.append(f"System: {content}")
417
+ elif role == 'user':
418
+ parts.append(f"User: {content}")
419
+ elif role == 'assistant':
420
+ parts.append(f"Assistant: {content}")
421
+ return "\n\n".join(parts)
422
+
423
+
424
+ def create_llm_client(config: Optional[Dict] = None) -> UnifiedLLMClient:
425
+ """
426
+ Factory function to create LLM client.
427
+
428
+ Zero-config: Just call this and it auto-detects providers.
429
+ """
430
+ return UnifiedLLMClient(config)