kailash 0.3.2__py3-none-any.whl → 0.4.1__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.
- kailash/__init__.py +33 -1
- kailash/access_control/__init__.py +129 -0
- kailash/access_control/managers.py +461 -0
- kailash/access_control/rule_evaluators.py +467 -0
- kailash/access_control_abac.py +825 -0
- kailash/config/__init__.py +27 -0
- kailash/config/database_config.py +359 -0
- kailash/database/__init__.py +28 -0
- kailash/database/execution_pipeline.py +499 -0
- kailash/middleware/__init__.py +306 -0
- kailash/middleware/auth/__init__.py +33 -0
- kailash/middleware/auth/access_control.py +436 -0
- kailash/middleware/auth/auth_manager.py +422 -0
- kailash/middleware/auth/jwt_auth.py +477 -0
- kailash/middleware/auth/kailash_jwt_auth.py +616 -0
- kailash/middleware/communication/__init__.py +37 -0
- kailash/middleware/communication/ai_chat.py +989 -0
- kailash/middleware/communication/api_gateway.py +802 -0
- kailash/middleware/communication/events.py +470 -0
- kailash/middleware/communication/realtime.py +710 -0
- kailash/middleware/core/__init__.py +21 -0
- kailash/middleware/core/agent_ui.py +890 -0
- kailash/middleware/core/schema.py +643 -0
- kailash/middleware/core/workflows.py +396 -0
- kailash/middleware/database/__init__.py +63 -0
- kailash/middleware/database/base.py +113 -0
- kailash/middleware/database/base_models.py +525 -0
- kailash/middleware/database/enums.py +106 -0
- kailash/middleware/database/migrations.py +12 -0
- kailash/{api/database.py → middleware/database/models.py} +183 -291
- kailash/middleware/database/repositories.py +685 -0
- kailash/middleware/database/session_manager.py +19 -0
- kailash/middleware/mcp/__init__.py +38 -0
- kailash/middleware/mcp/client_integration.py +585 -0
- kailash/middleware/mcp/enhanced_server.py +576 -0
- kailash/nodes/__init__.py +27 -3
- kailash/nodes/admin/__init__.py +42 -0
- kailash/nodes/admin/audit_log.py +794 -0
- kailash/nodes/admin/permission_check.py +864 -0
- kailash/nodes/admin/role_management.py +823 -0
- kailash/nodes/admin/security_event.py +1523 -0
- kailash/nodes/admin/user_management.py +944 -0
- kailash/nodes/ai/a2a.py +24 -7
- kailash/nodes/ai/ai_providers.py +248 -40
- kailash/nodes/ai/embedding_generator.py +11 -11
- kailash/nodes/ai/intelligent_agent_orchestrator.py +99 -11
- kailash/nodes/ai/llm_agent.py +436 -5
- kailash/nodes/ai/self_organizing.py +85 -10
- kailash/nodes/ai/vision_utils.py +148 -0
- kailash/nodes/alerts/__init__.py +26 -0
- kailash/nodes/alerts/base.py +234 -0
- kailash/nodes/alerts/discord.py +499 -0
- kailash/nodes/api/auth.py +287 -6
- kailash/nodes/api/rest.py +151 -0
- kailash/nodes/auth/__init__.py +17 -0
- kailash/nodes/auth/directory_integration.py +1228 -0
- kailash/nodes/auth/enterprise_auth_provider.py +1328 -0
- kailash/nodes/auth/mfa.py +2338 -0
- kailash/nodes/auth/risk_assessment.py +872 -0
- kailash/nodes/auth/session_management.py +1093 -0
- kailash/nodes/auth/sso.py +1040 -0
- kailash/nodes/base.py +344 -13
- kailash/nodes/base_cycle_aware.py +4 -2
- kailash/nodes/base_with_acl.py +1 -1
- kailash/nodes/code/python.py +283 -10
- kailash/nodes/compliance/__init__.py +9 -0
- kailash/nodes/compliance/data_retention.py +1888 -0
- kailash/nodes/compliance/gdpr.py +2004 -0
- kailash/nodes/data/__init__.py +22 -2
- kailash/nodes/data/async_connection.py +469 -0
- kailash/nodes/data/async_sql.py +757 -0
- kailash/nodes/data/async_vector.py +598 -0
- kailash/nodes/data/readers.py +767 -0
- kailash/nodes/data/retrieval.py +360 -1
- kailash/nodes/data/sharepoint_graph.py +397 -21
- kailash/nodes/data/sql.py +94 -5
- kailash/nodes/data/streaming.py +68 -8
- kailash/nodes/data/vector_db.py +54 -4
- kailash/nodes/enterprise/__init__.py +13 -0
- kailash/nodes/enterprise/batch_processor.py +741 -0
- kailash/nodes/enterprise/data_lineage.py +497 -0
- kailash/nodes/logic/convergence.py +31 -9
- kailash/nodes/logic/operations.py +14 -3
- kailash/nodes/mixins/__init__.py +8 -0
- kailash/nodes/mixins/event_emitter.py +201 -0
- kailash/nodes/mixins/mcp.py +9 -4
- kailash/nodes/mixins/security.py +165 -0
- kailash/nodes/monitoring/__init__.py +7 -0
- kailash/nodes/monitoring/performance_benchmark.py +2497 -0
- kailash/nodes/rag/__init__.py +284 -0
- kailash/nodes/rag/advanced.py +1615 -0
- kailash/nodes/rag/agentic.py +773 -0
- kailash/nodes/rag/conversational.py +999 -0
- kailash/nodes/rag/evaluation.py +875 -0
- kailash/nodes/rag/federated.py +1188 -0
- kailash/nodes/rag/graph.py +721 -0
- kailash/nodes/rag/multimodal.py +671 -0
- kailash/nodes/rag/optimized.py +933 -0
- kailash/nodes/rag/privacy.py +1059 -0
- kailash/nodes/rag/query_processing.py +1335 -0
- kailash/nodes/rag/realtime.py +764 -0
- kailash/nodes/rag/registry.py +547 -0
- kailash/nodes/rag/router.py +837 -0
- kailash/nodes/rag/similarity.py +1854 -0
- kailash/nodes/rag/strategies.py +566 -0
- kailash/nodes/rag/workflows.py +575 -0
- kailash/nodes/security/__init__.py +19 -0
- kailash/nodes/security/abac_evaluator.py +1411 -0
- kailash/nodes/security/audit_log.py +103 -0
- kailash/nodes/security/behavior_analysis.py +1893 -0
- kailash/nodes/security/credential_manager.py +401 -0
- kailash/nodes/security/rotating_credentials.py +760 -0
- kailash/nodes/security/security_event.py +133 -0
- kailash/nodes/security/threat_detection.py +1103 -0
- kailash/nodes/testing/__init__.py +9 -0
- kailash/nodes/testing/credential_testing.py +499 -0
- kailash/nodes/transform/__init__.py +10 -2
- kailash/nodes/transform/chunkers.py +592 -1
- kailash/nodes/transform/processors.py +484 -14
- kailash/nodes/validation.py +321 -0
- kailash/runtime/access_controlled.py +1 -1
- kailash/runtime/async_local.py +41 -7
- kailash/runtime/docker.py +1 -1
- kailash/runtime/local.py +474 -55
- kailash/runtime/parallel.py +1 -1
- kailash/runtime/parallel_cyclic.py +1 -1
- kailash/runtime/testing.py +210 -2
- kailash/security.py +1 -1
- kailash/utils/migrations/__init__.py +25 -0
- kailash/utils/migrations/generator.py +433 -0
- kailash/utils/migrations/models.py +231 -0
- kailash/utils/migrations/runner.py +489 -0
- kailash/utils/secure_logging.py +342 -0
- kailash/workflow/__init__.py +16 -0
- kailash/workflow/cyclic_runner.py +3 -4
- kailash/workflow/graph.py +70 -2
- kailash/workflow/resilience.py +249 -0
- kailash/workflow/templates.py +726 -0
- {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/METADATA +256 -20
- kailash-0.4.1.dist-info/RECORD +227 -0
- kailash/api/__init__.py +0 -17
- kailash/api/__main__.py +0 -6
- kailash/api/studio_secure.py +0 -893
- kailash/mcp/__main__.py +0 -13
- kailash/mcp/server_new.py +0 -336
- kailash/mcp/servers/__init__.py +0 -12
- kailash-0.3.2.dist-info/RECORD +0 -136
- {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/WHEEL +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/entry_points.txt +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/top_level.txt +0 -0
kailash/nodes/ai/llm_agent.py
CHANGED
@@ -1,16 +1,56 @@
|
|
1
1
|
"""Advanced LLM Agent node with LangChain integration and MCP support."""
|
2
2
|
|
3
3
|
import json
|
4
|
-
|
4
|
+
import time
|
5
|
+
from dataclasses import dataclass, field
|
6
|
+
from datetime import datetime, timezone
|
7
|
+
from typing import Any, Dict, List, Literal, Optional
|
5
8
|
|
6
9
|
from kailash.nodes.base import Node, NodeParameter, register_node
|
7
10
|
|
8
11
|
|
12
|
+
@dataclass
|
13
|
+
class TokenUsage:
|
14
|
+
"""Token usage statistics."""
|
15
|
+
|
16
|
+
prompt_tokens: int = 0
|
17
|
+
completion_tokens: int = 0
|
18
|
+
total_tokens: int = 0
|
19
|
+
|
20
|
+
def add(self, other: "TokenUsage"):
|
21
|
+
"""Add another usage record."""
|
22
|
+
self.prompt_tokens += other.prompt_tokens
|
23
|
+
self.completion_tokens += other.completion_tokens
|
24
|
+
self.total_tokens += other.total_tokens
|
25
|
+
|
26
|
+
|
27
|
+
@dataclass
|
28
|
+
class CostEstimate:
|
29
|
+
"""Cost estimation for LLM usage."""
|
30
|
+
|
31
|
+
prompt_cost: float = 0.0
|
32
|
+
completion_cost: float = 0.0
|
33
|
+
total_cost: float = 0.0
|
34
|
+
currency: str = "USD"
|
35
|
+
|
36
|
+
|
37
|
+
@dataclass
|
38
|
+
class UsageMetrics:
|
39
|
+
"""Comprehensive usage metrics."""
|
40
|
+
|
41
|
+
token_usage: TokenUsage = field(default_factory=TokenUsage)
|
42
|
+
cost_estimate: CostEstimate = field(default_factory=CostEstimate)
|
43
|
+
execution_time_ms: float = 0.0
|
44
|
+
model: str = ""
|
45
|
+
timestamp: str = ""
|
46
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
47
|
+
|
48
|
+
|
9
49
|
@register_node()
|
10
50
|
class LLMAgentNode(Node):
|
11
51
|
"""
|
12
52
|
Advanced Large Language Model agent with LangChain integration and MCP
|
13
|
-
support.
|
53
|
+
support, with optional cost tracking and usage monitoring.
|
14
54
|
|
15
55
|
Design Purpose and Philosophy:
|
16
56
|
The LLMAgent node provides enterprise-grade AI agent capabilities with
|
@@ -119,6 +159,57 @@ class LLMAgentNode(Node):
|
|
119
159
|
... )
|
120
160
|
"""
|
121
161
|
|
162
|
+
# Model pricing (USD per 1K tokens)
|
163
|
+
MODEL_PRICING = {
|
164
|
+
# OpenAI models
|
165
|
+
"gpt-4": {"prompt": 0.03, "completion": 0.06},
|
166
|
+
"gpt-4-turbo": {"prompt": 0.01, "completion": 0.03},
|
167
|
+
"gpt-3.5-turbo": {"prompt": 0.001, "completion": 0.002},
|
168
|
+
"gpt-3.5-turbo-16k": {"prompt": 0.003, "completion": 0.004},
|
169
|
+
# Anthropic models
|
170
|
+
"claude-3-opus": {"prompt": 0.015, "completion": 0.075},
|
171
|
+
"claude-3-sonnet": {"prompt": 0.003, "completion": 0.015},
|
172
|
+
"claude-3-haiku": {"prompt": 0.00025, "completion": 0.00125},
|
173
|
+
"claude-2.1": {"prompt": 0.008, "completion": 0.024},
|
174
|
+
# Google models
|
175
|
+
"gemini-pro": {"prompt": 0.00025, "completion": 0.0005},
|
176
|
+
"gemini-pro-vision": {"prompt": 0.00025, "completion": 0.0005},
|
177
|
+
# Cohere models
|
178
|
+
"command": {"prompt": 0.0015, "completion": 0.0015},
|
179
|
+
"command-light": {"prompt": 0.0006, "completion": 0.0006},
|
180
|
+
}
|
181
|
+
|
182
|
+
def __init__(self, **kwargs):
|
183
|
+
"""Initialize LLMAgentNode with optional monitoring features.
|
184
|
+
|
185
|
+
Args:
|
186
|
+
enable_monitoring: Enable token usage and cost tracking
|
187
|
+
budget_limit: Maximum spend allowed in USD (None = unlimited)
|
188
|
+
alert_threshold: Alert when usage reaches this fraction of budget
|
189
|
+
track_history: Whether to keep usage history
|
190
|
+
history_limit: Maximum history entries to keep
|
191
|
+
custom_pricing: Override default pricing (per 1K tokens)
|
192
|
+
cost_multiplier: Multiply all costs by this factor
|
193
|
+
**kwargs: Additional Node parameters
|
194
|
+
"""
|
195
|
+
super().__init__(**kwargs)
|
196
|
+
|
197
|
+
# Monitoring configuration
|
198
|
+
self.enable_monitoring = kwargs.get("enable_monitoring", False)
|
199
|
+
self.budget_limit = kwargs.get("budget_limit")
|
200
|
+
self.alert_threshold = kwargs.get("alert_threshold", 0.8)
|
201
|
+
self.track_history = kwargs.get("track_history", True)
|
202
|
+
self.history_limit = kwargs.get("history_limit", 1000)
|
203
|
+
self.custom_pricing = kwargs.get("custom_pricing")
|
204
|
+
self.cost_multiplier = kwargs.get("cost_multiplier", 1.0)
|
205
|
+
|
206
|
+
# Usage tracking (only if monitoring enabled)
|
207
|
+
if self.enable_monitoring:
|
208
|
+
self._total_usage = TokenUsage()
|
209
|
+
self._total_cost = 0.0
|
210
|
+
self._usage_history: List[UsageMetrics] = []
|
211
|
+
self._budget_alerts_sent = False
|
212
|
+
|
122
213
|
def get_parameters(self) -> dict[str, NodeParameter]:
|
123
214
|
return {
|
124
215
|
"provider": NodeParameter(
|
@@ -224,6 +315,40 @@ class LLMAgentNode(Node):
|
|
224
315
|
default=3,
|
225
316
|
description="Maximum retry attempts for failed requests",
|
226
317
|
),
|
318
|
+
# Monitoring parameters
|
319
|
+
"enable_monitoring": NodeParameter(
|
320
|
+
name="enable_monitoring",
|
321
|
+
type=bool,
|
322
|
+
required=False,
|
323
|
+
default=False,
|
324
|
+
description="Enable token usage tracking and cost monitoring",
|
325
|
+
),
|
326
|
+
"budget_limit": NodeParameter(
|
327
|
+
name="budget_limit",
|
328
|
+
type=float,
|
329
|
+
required=False,
|
330
|
+
description="Maximum spend allowed in USD (None = unlimited)",
|
331
|
+
),
|
332
|
+
"alert_threshold": NodeParameter(
|
333
|
+
name="alert_threshold",
|
334
|
+
type=float,
|
335
|
+
required=False,
|
336
|
+
default=0.8,
|
337
|
+
description="Alert when usage reaches this fraction of budget",
|
338
|
+
),
|
339
|
+
"track_history": NodeParameter(
|
340
|
+
name="track_history",
|
341
|
+
type=bool,
|
342
|
+
required=False,
|
343
|
+
default=True,
|
344
|
+
description="Whether to keep usage history for analytics",
|
345
|
+
),
|
346
|
+
"custom_pricing": NodeParameter(
|
347
|
+
name="custom_pricing",
|
348
|
+
type=dict,
|
349
|
+
required=False,
|
350
|
+
description="Override default model pricing (per 1K tokens)",
|
351
|
+
),
|
227
352
|
}
|
228
353
|
|
229
354
|
def run(self, **kwargs) -> dict[str, Any]:
|
@@ -465,6 +590,19 @@ class LLMAgentNode(Node):
|
|
465
590
|
timeout = kwargs.get("timeout", 120)
|
466
591
|
max_retries = kwargs.get("max_retries", 3)
|
467
592
|
|
593
|
+
# Check monitoring parameters
|
594
|
+
enable_monitoring = kwargs.get("enable_monitoring", self.enable_monitoring)
|
595
|
+
|
596
|
+
# Check budget if monitoring is enabled
|
597
|
+
if enable_monitoring and not self._check_budget():
|
598
|
+
raise ValueError(
|
599
|
+
f"Budget limit exceeded: ${self._total_cost:.2f}/${self.budget_limit:.2f} USD. "
|
600
|
+
"Reset budget or increase limit to continue."
|
601
|
+
)
|
602
|
+
|
603
|
+
# Track execution time
|
604
|
+
start_time = time.time()
|
605
|
+
|
468
606
|
try:
|
469
607
|
# Import LangChain and related libraries (graceful fallback)
|
470
608
|
langchain_available = self._check_langchain_availability()
|
@@ -530,6 +668,47 @@ class LLMAgentNode(Node):
|
|
530
668
|
enriched_messages, response, model, provider
|
531
669
|
)
|
532
670
|
|
671
|
+
# Add monitoring data if enabled
|
672
|
+
execution_time = time.time() - start_time
|
673
|
+
if enable_monitoring:
|
674
|
+
# Extract token usage for monitoring
|
675
|
+
usage = self._extract_token_usage(response)
|
676
|
+
cost = self._calculate_cost(usage, model)
|
677
|
+
|
678
|
+
# Update totals
|
679
|
+
if hasattr(self, "_total_usage"):
|
680
|
+
self._total_usage.add(usage)
|
681
|
+
self._total_cost += cost.total_cost
|
682
|
+
|
683
|
+
# Record metrics
|
684
|
+
self._record_usage(usage, cost, execution_time, model)
|
685
|
+
|
686
|
+
# Add monitoring section to response
|
687
|
+
usage_metrics["monitoring"] = {
|
688
|
+
"tokens": {
|
689
|
+
"prompt": usage.prompt_tokens,
|
690
|
+
"completion": usage.completion_tokens,
|
691
|
+
"total": usage.total_tokens,
|
692
|
+
},
|
693
|
+
"cost": {
|
694
|
+
"prompt": round(cost.prompt_cost, 6),
|
695
|
+
"completion": round(cost.completion_cost, 6),
|
696
|
+
"total": round(cost.total_cost, 6),
|
697
|
+
"currency": cost.currency,
|
698
|
+
},
|
699
|
+
"execution_time_ms": round(execution_time * 1000, 2),
|
700
|
+
"model": model,
|
701
|
+
"budget": {
|
702
|
+
"used": round(self._total_cost, 4),
|
703
|
+
"limit": self.budget_limit,
|
704
|
+
"remaining": (
|
705
|
+
round(self.budget_limit - self._total_cost, 4)
|
706
|
+
if self.budget_limit
|
707
|
+
else None
|
708
|
+
),
|
709
|
+
},
|
710
|
+
}
|
711
|
+
|
533
712
|
return {
|
534
713
|
"success": True,
|
535
714
|
"response": response,
|
@@ -1233,13 +1412,28 @@ class LLMAgentNode(Node):
|
|
1233
1412
|
) -> dict[str, Any]:
|
1234
1413
|
"""Generate mock LLM response for testing."""
|
1235
1414
|
last_user_message = ""
|
1415
|
+
has_images = False
|
1416
|
+
|
1236
1417
|
for msg in reversed(messages):
|
1237
1418
|
if msg.get("role") == "user":
|
1238
|
-
|
1419
|
+
content = msg.get("content", "")
|
1420
|
+
# Handle complex content with images
|
1421
|
+
if isinstance(content, list):
|
1422
|
+
text_parts = []
|
1423
|
+
for item in content:
|
1424
|
+
if item.get("type") == "text":
|
1425
|
+
text_parts.append(item.get("text", ""))
|
1426
|
+
elif item.get("type") == "image":
|
1427
|
+
has_images = True
|
1428
|
+
last_user_message = " ".join(text_parts)
|
1429
|
+
else:
|
1430
|
+
last_user_message = content
|
1239
1431
|
break
|
1240
1432
|
|
1241
1433
|
# Generate contextual mock response
|
1242
|
-
if
|
1434
|
+
if has_images:
|
1435
|
+
response_content = "I can see the image(s) you've provided. Based on my analysis, [Mock vision response for testing]"
|
1436
|
+
elif "analyze" in last_user_message.lower():
|
1243
1437
|
response_content = "Based on the provided data and context, I can see several key patterns: 1) Customer engagement has increased by 15% this quarter, 2) Product A shows the highest conversion rate, and 3) There are opportunities for improvement in the onboarding process."
|
1244
1438
|
elif (
|
1245
1439
|
"create" in last_user_message.lower()
|
@@ -1279,7 +1473,18 @@ class LLMAgentNode(Node):
|
|
1279
1473
|
"finish_reason": "stop" if not tool_calls else "tool_calls",
|
1280
1474
|
"usage": {
|
1281
1475
|
"prompt_tokens": len(
|
1282
|
-
" ".join(
|
1476
|
+
" ".join(
|
1477
|
+
(
|
1478
|
+
msg.get("content", "")
|
1479
|
+
if isinstance(msg.get("content"), str)
|
1480
|
+
else " ".join(
|
1481
|
+
item.get("text", "")
|
1482
|
+
for item in msg.get("content", [])
|
1483
|
+
if item.get("type") == "text"
|
1484
|
+
)
|
1485
|
+
)
|
1486
|
+
for msg in messages
|
1487
|
+
)
|
1283
1488
|
)
|
1284
1489
|
// 4,
|
1285
1490
|
"completion_tokens": len(response_content) // 4,
|
@@ -1481,3 +1686,229 @@ class LLMAgentNode(Node):
|
|
1481
1686
|
except Exception as e:
|
1482
1687
|
self.logger.error(f"MCP tool execution failed: {e}")
|
1483
1688
|
return {"error": str(e), "success": False, "tool_name": tool_name}
|
1689
|
+
|
1690
|
+
# Monitoring methods
|
1691
|
+
def _get_pricing(self, model: str) -> Dict[str, float]:
|
1692
|
+
"""Get pricing for current model."""
|
1693
|
+
if self.custom_pricing:
|
1694
|
+
return {
|
1695
|
+
"prompt": self.custom_pricing.get("prompt_token_cost", 0.001),
|
1696
|
+
"completion": self.custom_pricing.get("completion_token_cost", 0.002),
|
1697
|
+
}
|
1698
|
+
|
1699
|
+
# Check if model has pricing info
|
1700
|
+
model_key = None
|
1701
|
+
for key in self.MODEL_PRICING:
|
1702
|
+
if key in model.lower():
|
1703
|
+
model_key = key
|
1704
|
+
break
|
1705
|
+
|
1706
|
+
if model_key:
|
1707
|
+
return self.MODEL_PRICING[model_key]
|
1708
|
+
|
1709
|
+
# Default pricing if model not found
|
1710
|
+
return {"prompt": 0.001, "completion": 0.002}
|
1711
|
+
|
1712
|
+
def _calculate_cost(self, usage: TokenUsage, model: str) -> CostEstimate:
|
1713
|
+
"""Calculate cost from token usage."""
|
1714
|
+
pricing = self._get_pricing(model)
|
1715
|
+
|
1716
|
+
# Cost per 1K tokens
|
1717
|
+
prompt_cost = (
|
1718
|
+
(usage.prompt_tokens / 1000) * pricing["prompt"] * self.cost_multiplier
|
1719
|
+
)
|
1720
|
+
completion_cost = (
|
1721
|
+
(usage.completion_tokens / 1000)
|
1722
|
+
* pricing["completion"]
|
1723
|
+
* self.cost_multiplier
|
1724
|
+
)
|
1725
|
+
|
1726
|
+
return CostEstimate(
|
1727
|
+
prompt_cost=prompt_cost,
|
1728
|
+
completion_cost=completion_cost,
|
1729
|
+
total_cost=prompt_cost + completion_cost,
|
1730
|
+
currency="USD",
|
1731
|
+
)
|
1732
|
+
|
1733
|
+
def _extract_token_usage(self, response: Dict[str, Any]) -> TokenUsage:
|
1734
|
+
"""Extract token usage from LLM response."""
|
1735
|
+
usage = TokenUsage()
|
1736
|
+
|
1737
|
+
# Check if response has usage data
|
1738
|
+
if "usage" in response:
|
1739
|
+
usage_data = response["usage"]
|
1740
|
+
usage.prompt_tokens = usage_data.get("prompt_tokens", 0)
|
1741
|
+
usage.completion_tokens = usage_data.get("completion_tokens", 0)
|
1742
|
+
usage.total_tokens = usage_data.get("total_tokens", 0)
|
1743
|
+
|
1744
|
+
# Anthropic format
|
1745
|
+
elif "metadata" in response and "usage" in response["metadata"]:
|
1746
|
+
usage_data = response["metadata"]["usage"]
|
1747
|
+
usage.prompt_tokens = usage_data.get("input_tokens", 0)
|
1748
|
+
usage.completion_tokens = usage_data.get("output_tokens", 0)
|
1749
|
+
usage.total_tokens = usage.prompt_tokens + usage.completion_tokens
|
1750
|
+
|
1751
|
+
# Fallback: estimate from text length
|
1752
|
+
elif "content" in response or "text" in response:
|
1753
|
+
text = response.get("content") or response.get("text", "")
|
1754
|
+
# Rough estimation: 1 token ≈ 4 characters
|
1755
|
+
usage.completion_tokens = len(text) // 4
|
1756
|
+
usage.prompt_tokens = 100 # Rough estimate
|
1757
|
+
usage.total_tokens = usage.prompt_tokens + usage.completion_tokens
|
1758
|
+
|
1759
|
+
return usage
|
1760
|
+
|
1761
|
+
def _check_budget(self) -> bool:
|
1762
|
+
"""Check if within budget. Returns True if OK to proceed."""
|
1763
|
+
if not self.budget_limit or not hasattr(self, "_total_cost"):
|
1764
|
+
return True
|
1765
|
+
|
1766
|
+
if self._total_cost >= self.budget_limit:
|
1767
|
+
return False
|
1768
|
+
|
1769
|
+
# Check alert threshold
|
1770
|
+
if (
|
1771
|
+
not self._budget_alerts_sent
|
1772
|
+
and self._total_cost >= self.budget_limit * self.alert_threshold
|
1773
|
+
):
|
1774
|
+
self._budget_alerts_sent = True
|
1775
|
+
# In production, this would send actual alerts
|
1776
|
+
self.logger.warning(
|
1777
|
+
f"Budget Alert: ${self._total_cost:.2f}/${self.budget_limit:.2f} USD used "
|
1778
|
+
f"({self._total_cost/self.budget_limit*100:.1f}%)"
|
1779
|
+
)
|
1780
|
+
|
1781
|
+
return True
|
1782
|
+
|
1783
|
+
def _record_usage(
|
1784
|
+
self, usage: TokenUsage, cost: CostEstimate, execution_time: float, model: str
|
1785
|
+
):
|
1786
|
+
"""Record usage metrics."""
|
1787
|
+
if not self.track_history or not hasattr(self, "_usage_history"):
|
1788
|
+
return
|
1789
|
+
|
1790
|
+
metrics = UsageMetrics(
|
1791
|
+
token_usage=usage,
|
1792
|
+
cost_estimate=cost,
|
1793
|
+
execution_time_ms=execution_time * 1000,
|
1794
|
+
model=model,
|
1795
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
1796
|
+
metadata={
|
1797
|
+
"node_id": self.id,
|
1798
|
+
"budget_remaining": (
|
1799
|
+
self.budget_limit - self._total_cost if self.budget_limit else None
|
1800
|
+
),
|
1801
|
+
},
|
1802
|
+
)
|
1803
|
+
|
1804
|
+
self._usage_history.append(metrics)
|
1805
|
+
|
1806
|
+
# Maintain history limit
|
1807
|
+
if len(self._usage_history) > self.history_limit:
|
1808
|
+
self._usage_history.pop(0)
|
1809
|
+
|
1810
|
+
def get_usage_report(self) -> Dict[str, Any]:
|
1811
|
+
"""Get comprehensive usage report."""
|
1812
|
+
if not self.enable_monitoring or not hasattr(self, "_total_usage"):
|
1813
|
+
return {"error": "Monitoring not enabled"}
|
1814
|
+
|
1815
|
+
report = {
|
1816
|
+
"summary": {
|
1817
|
+
"total_tokens": self._total_usage.total_tokens,
|
1818
|
+
"prompt_tokens": self._total_usage.prompt_tokens,
|
1819
|
+
"completion_tokens": self._total_usage.completion_tokens,
|
1820
|
+
"total_cost": round(self._total_cost, 4),
|
1821
|
+
"currency": "USD",
|
1822
|
+
"requests": (
|
1823
|
+
len(self._usage_history) if hasattr(self, "_usage_history") else 0
|
1824
|
+
),
|
1825
|
+
},
|
1826
|
+
"budget": {
|
1827
|
+
"limit": self.budget_limit,
|
1828
|
+
"used": round(self._total_cost, 4),
|
1829
|
+
"remaining": (
|
1830
|
+
round(self.budget_limit - self._total_cost, 4)
|
1831
|
+
if self.budget_limit
|
1832
|
+
else None
|
1833
|
+
),
|
1834
|
+
"percentage_used": (
|
1835
|
+
round(self._total_cost / self.budget_limit * 100, 1)
|
1836
|
+
if self.budget_limit
|
1837
|
+
else 0
|
1838
|
+
),
|
1839
|
+
},
|
1840
|
+
}
|
1841
|
+
|
1842
|
+
if hasattr(self, "_usage_history") and self._usage_history:
|
1843
|
+
# Calculate analytics
|
1844
|
+
total_time = sum(m.execution_time_ms for m in self._usage_history)
|
1845
|
+
avg_time = total_time / len(self._usage_history)
|
1846
|
+
|
1847
|
+
report["analytics"] = {
|
1848
|
+
"average_tokens_per_request": self._total_usage.total_tokens
|
1849
|
+
// len(self._usage_history),
|
1850
|
+
"average_cost_per_request": round(
|
1851
|
+
self._total_cost / len(self._usage_history), 4
|
1852
|
+
),
|
1853
|
+
"average_execution_time_ms": round(avg_time, 2),
|
1854
|
+
"cost_per_1k_tokens": (
|
1855
|
+
round(self._total_cost / (self._total_usage.total_tokens / 1000), 4)
|
1856
|
+
if self._total_usage.total_tokens > 0
|
1857
|
+
else 0
|
1858
|
+
),
|
1859
|
+
}
|
1860
|
+
|
1861
|
+
# Recent history
|
1862
|
+
report["recent_usage"] = [
|
1863
|
+
{
|
1864
|
+
"timestamp": m.timestamp,
|
1865
|
+
"tokens": m.token_usage.total_tokens,
|
1866
|
+
"cost": round(m.cost_estimate.total_cost, 6),
|
1867
|
+
"execution_time_ms": round(m.execution_time_ms, 2),
|
1868
|
+
}
|
1869
|
+
for m in self._usage_history[-10:] # Last 10 requests
|
1870
|
+
]
|
1871
|
+
|
1872
|
+
return report
|
1873
|
+
|
1874
|
+
def reset_budget(self):
|
1875
|
+
"""Reset budget tracking."""
|
1876
|
+
if hasattr(self, "_total_cost"):
|
1877
|
+
self._total_cost = 0.0
|
1878
|
+
self._budget_alerts_sent = False
|
1879
|
+
|
1880
|
+
def reset_usage(self):
|
1881
|
+
"""Reset all usage tracking."""
|
1882
|
+
if hasattr(self, "_total_usage"):
|
1883
|
+
self._total_usage = TokenUsage()
|
1884
|
+
self._total_cost = 0.0
|
1885
|
+
self._usage_history = []
|
1886
|
+
self._budget_alerts_sent = False
|
1887
|
+
|
1888
|
+
def export_usage_data(self, format: Literal["json", "csv"] = "json") -> str:
|
1889
|
+
"""Export usage data for analysis."""
|
1890
|
+
if not self.enable_monitoring:
|
1891
|
+
return json.dumps({"error": "Monitoring not enabled"})
|
1892
|
+
|
1893
|
+
if format == "json":
|
1894
|
+
return json.dumps(self.get_usage_report(), indent=2)
|
1895
|
+
|
1896
|
+
elif format == "csv":
|
1897
|
+
if not hasattr(self, "_usage_history"):
|
1898
|
+
return "timestamp,model,prompt_tokens,completion_tokens,total_tokens,cost,execution_time_ms"
|
1899
|
+
|
1900
|
+
# Simple CSV export
|
1901
|
+
lines = [
|
1902
|
+
"timestamp,model,prompt_tokens,completion_tokens,total_tokens,cost,execution_time_ms"
|
1903
|
+
]
|
1904
|
+
for m in self._usage_history:
|
1905
|
+
lines.append(
|
1906
|
+
f"{m.timestamp},{m.model},{m.token_usage.prompt_tokens},"
|
1907
|
+
f"{m.token_usage.completion_tokens},{m.token_usage.total_tokens},"
|
1908
|
+
f"{m.cost_estimate.total_cost:.6f},{m.execution_time_ms:.2f}"
|
1909
|
+
)
|
1910
|
+
return "\n".join(lines)
|
1911
|
+
|
1912
|
+
async def async_run(self, **kwargs) -> Dict[str, Any]:
|
1913
|
+
"""Async execution method for enterprise integration."""
|
1914
|
+
return self.run(**kwargs)
|
@@ -109,8 +109,20 @@ class AgentPoolManagerNode(Node):
|
|
109
109
|
>>> assert result["success"] == True
|
110
110
|
"""
|
111
111
|
|
112
|
-
def __init__(self):
|
113
|
-
|
112
|
+
def __init__(self, name: str = None, id: str = None, **kwargs):
|
113
|
+
# Set name from parameters
|
114
|
+
if name:
|
115
|
+
self.name = name
|
116
|
+
elif id:
|
117
|
+
self.name = id
|
118
|
+
elif "name" in kwargs:
|
119
|
+
self.name = kwargs.pop("name")
|
120
|
+
elif "id" in kwargs:
|
121
|
+
self.name = kwargs.pop("id")
|
122
|
+
else:
|
123
|
+
self.name = self.__class__.__name__
|
124
|
+
|
125
|
+
# Initialize node attributes
|
114
126
|
self.agent_registry = {}
|
115
127
|
self.availability_tracker = {}
|
116
128
|
self.performance_metrics = defaultdict(
|
@@ -125,6 +137,9 @@ class AgentPoolManagerNode(Node):
|
|
125
137
|
self.capability_index = defaultdict(set)
|
126
138
|
self.team_history = deque(maxlen=100)
|
127
139
|
|
140
|
+
# Call parent constructor
|
141
|
+
super().__init__(name=self.name)
|
142
|
+
|
128
143
|
def get_parameters(self) -> dict[str, NodeParameter]:
|
129
144
|
return {
|
130
145
|
"action": NodeParameter(
|
@@ -484,8 +499,20 @@ class ProblemAnalyzerNode(Node):
|
|
484
499
|
>>> assert "decomposition_strategy" in params
|
485
500
|
"""
|
486
501
|
|
487
|
-
def __init__(self):
|
488
|
-
|
502
|
+
def __init__(self, name: str = None, id: str = None, **kwargs):
|
503
|
+
# Set name from parameters
|
504
|
+
if name:
|
505
|
+
self.name = name
|
506
|
+
elif id:
|
507
|
+
self.name = id
|
508
|
+
elif "name" in kwargs:
|
509
|
+
self.name = kwargs.pop("name")
|
510
|
+
elif "id" in kwargs:
|
511
|
+
self.name = kwargs.pop("id")
|
512
|
+
else:
|
513
|
+
self.name = self.__class__.__name__
|
514
|
+
|
515
|
+
# Initialize node attributes
|
489
516
|
self.capability_patterns = {
|
490
517
|
"data": ["data_collection", "data_cleaning", "data_validation"],
|
491
518
|
"analysis": [
|
@@ -499,6 +526,9 @@ class ProblemAnalyzerNode(Node):
|
|
499
526
|
"domain": ["domain_expertise", "validation", "interpretation"],
|
500
527
|
}
|
501
528
|
|
529
|
+
# Call parent constructor
|
530
|
+
super().__init__(name=self.name)
|
531
|
+
|
502
532
|
def get_parameters(self) -> dict[str, NodeParameter]:
|
503
533
|
return {
|
504
534
|
"problem_description": NodeParameter(
|
@@ -784,11 +814,26 @@ class TeamFormationNode(Node):
|
|
784
814
|
... )
|
785
815
|
"""
|
786
816
|
|
787
|
-
def __init__(self):
|
788
|
-
|
817
|
+
def __init__(self, name: str = None, id: str = None, **kwargs):
|
818
|
+
# Set name from parameters
|
819
|
+
if name:
|
820
|
+
self.name = name
|
821
|
+
elif id:
|
822
|
+
self.name = id
|
823
|
+
elif "name" in kwargs:
|
824
|
+
self.name = kwargs.pop("name")
|
825
|
+
elif "id" in kwargs:
|
826
|
+
self.name = kwargs.pop("id")
|
827
|
+
else:
|
828
|
+
self.name = self.__class__.__name__
|
829
|
+
|
830
|
+
# Initialize node attributes
|
789
831
|
self.formation_history = deque(maxlen=50)
|
790
832
|
self.team_performance_cache = {}
|
791
833
|
|
834
|
+
# Call parent constructor
|
835
|
+
super().__init__(name=self.name)
|
836
|
+
|
792
837
|
def get_parameters(self) -> dict[str, NodeParameter]:
|
793
838
|
return {
|
794
839
|
"problem_analysis": NodeParameter(
|
@@ -1225,12 +1270,27 @@ class SelfOrganizingAgentNode(A2AAgentNode):
|
|
1225
1270
|
>>> assert "capabilities" in params
|
1226
1271
|
"""
|
1227
1272
|
|
1228
|
-
def __init__(self):
|
1229
|
-
|
1273
|
+
def __init__(self, name: str = None, id: str = None, **kwargs):
|
1274
|
+
# Set name from parameters
|
1275
|
+
if name:
|
1276
|
+
self.name = name
|
1277
|
+
elif id:
|
1278
|
+
self.name = id
|
1279
|
+
elif "name" in kwargs:
|
1280
|
+
self.name = kwargs.pop("name")
|
1281
|
+
elif "id" in kwargs:
|
1282
|
+
self.name = kwargs.pop("id")
|
1283
|
+
else:
|
1284
|
+
self.name = self.__class__.__name__
|
1285
|
+
|
1286
|
+
# Initialize node attributes
|
1230
1287
|
self.team_memberships = {}
|
1231
1288
|
self.collaboration_history = deque(maxlen=50)
|
1232
1289
|
self.skill_adaptations = defaultdict(float)
|
1233
1290
|
|
1291
|
+
# Call parent constructor
|
1292
|
+
super().__init__(name=self.name)
|
1293
|
+
|
1234
1294
|
def get_parameters(self) -> dict[str, NodeParameter]:
|
1235
1295
|
params = super().get_parameters()
|
1236
1296
|
|
@@ -1438,10 +1498,25 @@ class SolutionEvaluatorNode(Node):
|
|
1438
1498
|
... )
|
1439
1499
|
"""
|
1440
1500
|
|
1441
|
-
def __init__(self):
|
1442
|
-
|
1501
|
+
def __init__(self, name: str = None, id: str = None, **kwargs):
|
1502
|
+
# Set name from parameters
|
1503
|
+
if name:
|
1504
|
+
self.name = name
|
1505
|
+
elif id:
|
1506
|
+
self.name = id
|
1507
|
+
elif "name" in kwargs:
|
1508
|
+
self.name = kwargs.pop("name")
|
1509
|
+
elif "id" in kwargs:
|
1510
|
+
self.name = kwargs.pop("id")
|
1511
|
+
else:
|
1512
|
+
self.name = self.__class__.__name__
|
1513
|
+
|
1514
|
+
# Initialize node attributes
|
1443
1515
|
self.evaluation_history = deque(maxlen=100)
|
1444
1516
|
|
1517
|
+
# Call parent constructor
|
1518
|
+
super().__init__(name=self.name)
|
1519
|
+
|
1445
1520
|
def get_parameters(self) -> dict[str, NodeParameter]:
|
1446
1521
|
return {
|
1447
1522
|
"solution": NodeParameter(
|