machinaos 0.0.21 → 0.0.23
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.
- package/README.md +32 -6
- package/bin/cli.js +0 -0
- package/client/dist/assets/index-5BWZnM6b.js +703 -0
- package/client/dist/index.html +1 -1
- package/client/package.json +1 -1
- package/client/src/Dashboard.tsx +12 -5
- package/client/src/ParameterPanel.tsx +6 -5
- package/client/src/components/AIAgentNode.tsx +35 -16
- package/client/src/components/CredentialsModal.tsx +450 -5
- package/client/src/components/TeamMonitorNode.tsx +269 -0
- package/client/src/components/parameterPanel/InputSection.tsx +25 -0
- package/client/src/contexts/WebSocketContext.tsx +38 -0
- package/client/src/hooks/useApiKeys.ts +44 -0
- package/client/src/nodeDefinitions/specializedAgentNodes.ts +59 -3
- package/client/src/nodeDefinitions/twitterNodes.ts +441 -0
- package/client/src/nodeDefinitions/utilityNodes.ts +45 -1
- package/client/src/nodeDefinitions.ts +7 -1
- package/client/src/services/executionService.ts +4 -1
- package/install.sh +63 -1
- package/package.json +5 -2
- package/scripts/build.js +0 -0
- package/scripts/clean.js +0 -0
- package/scripts/daemon.js +0 -0
- package/scripts/docker.js +0 -0
- package/scripts/install.js +0 -0
- package/scripts/postinstall.js +29 -0
- package/scripts/preinstall.js +67 -0
- package/scripts/serve-client.js +0 -0
- package/scripts/start.js +0 -0
- package/scripts/stop.js +0 -0
- package/scripts/sync-version.js +0 -0
- package/server/Dockerfile +10 -15
- package/server/constants.py +20 -0
- package/server/core/database.py +443 -3
- package/server/main.py +9 -1
- package/server/models/database.py +112 -2
- package/server/pyproject.toml +3 -0
- package/server/requirements.txt +3 -0
- package/server/routers/twitter.py +390 -0
- package/server/routers/websocket.py +320 -0
- package/server/services/agent_team.py +266 -0
- package/server/services/ai.py +43 -0
- package/server/services/compaction.py +39 -4
- package/server/services/event_waiter.py +41 -0
- package/server/services/handlers/__init__.py +13 -0
- package/server/services/handlers/ai.py +66 -2
- package/server/services/handlers/tools.py +84 -0
- package/server/services/handlers/twitter.py +297 -0
- package/server/services/handlers/utility.py +91 -0
- package/server/services/node_executor.py +15 -1
- package/server/services/pricing.py +270 -0
- package/server/services/status_broadcaster.py +79 -0
- package/server/services/twitter_oauth.py +410 -0
- package/server/skills/social_agent/twitter-search-skill/SKILL.md +146 -0
- package/server/skills/social_agent/twitter-send-skill/SKILL.md +142 -0
- package/server/skills/social_agent/twitter-user-skill/SKILL.md +165 -0
- package/workflows/Zeenie_full.json +459 -0
- package/workflows/Zeenie_small.json +459 -0
- package/client/dist/assets/index-YVvAiByx.js +0 -703
- package/server/requirements-docker.txt +0 -86
|
@@ -820,3 +820,94 @@ def _format_console_output(data: Any, format_type: str) -> str:
|
|
|
820
820
|
return json.dumps(data, indent=2, default=str, ensure_ascii=False)
|
|
821
821
|
case _:
|
|
822
822
|
return str(data)
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
# =============================================================================
|
|
826
|
+
# TEAM MONITOR HANDLER
|
|
827
|
+
# =============================================================================
|
|
828
|
+
|
|
829
|
+
async def handle_team_monitor(
|
|
830
|
+
node_id: str,
|
|
831
|
+
node_type: str,
|
|
832
|
+
parameters: Dict[str, Any],
|
|
833
|
+
context: Dict[str, Any]
|
|
834
|
+
) -> Dict[str, Any]:
|
|
835
|
+
"""Handle team monitor node - returns team status snapshot.
|
|
836
|
+
|
|
837
|
+
Args:
|
|
838
|
+
node_id: The node ID
|
|
839
|
+
node_type: The node type (teamMonitor)
|
|
840
|
+
parameters: Resolved parameters
|
|
841
|
+
context: Execution context with team_id from connected team node
|
|
842
|
+
|
|
843
|
+
Returns:
|
|
844
|
+
Execution result dict with team status
|
|
845
|
+
"""
|
|
846
|
+
from services.agent_team import get_agent_team_service
|
|
847
|
+
start_time = time.time()
|
|
848
|
+
|
|
849
|
+
try:
|
|
850
|
+
# Get team_id from context (set by connected team lead node)
|
|
851
|
+
team_id = context.get('team_id')
|
|
852
|
+
|
|
853
|
+
# Try to get from connected node outputs
|
|
854
|
+
if not team_id:
|
|
855
|
+
outputs = context.get('outputs', {})
|
|
856
|
+
for output in outputs.values():
|
|
857
|
+
if isinstance(output, dict) and output.get('team_id'):
|
|
858
|
+
team_id = output.get('team_id')
|
|
859
|
+
break
|
|
860
|
+
|
|
861
|
+
if not team_id:
|
|
862
|
+
return {
|
|
863
|
+
"success": True,
|
|
864
|
+
"node_id": node_id,
|
|
865
|
+
"node_type": "teamMonitor",
|
|
866
|
+
"result": {
|
|
867
|
+
"message": "No team connected",
|
|
868
|
+
"team_id": None,
|
|
869
|
+
"members": [],
|
|
870
|
+
"tasks": {"total": 0, "completed": 0, "active": 0, "pending": 0, "failed": 0},
|
|
871
|
+
"active_tasks": [],
|
|
872
|
+
"recent_events": []
|
|
873
|
+
},
|
|
874
|
+
"execution_time": time.time() - start_time,
|
|
875
|
+
"timestamp": datetime.now().isoformat()
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
team_service = get_agent_team_service()
|
|
879
|
+
status = await team_service.get_team_status(team_id)
|
|
880
|
+
|
|
881
|
+
max_history = parameters.get('maxHistoryItems', 50)
|
|
882
|
+
|
|
883
|
+
return {
|
|
884
|
+
"success": True,
|
|
885
|
+
"node_id": node_id,
|
|
886
|
+
"node_type": "teamMonitor",
|
|
887
|
+
"result": {
|
|
888
|
+
"team_id": team_id,
|
|
889
|
+
"members": status.get('members', []),
|
|
890
|
+
"tasks": {
|
|
891
|
+
"total": status.get('task_count', 0),
|
|
892
|
+
"completed": status.get('completed_count', 0),
|
|
893
|
+
"active": status.get('active_count', 0),
|
|
894
|
+
"pending": status.get('pending_count', 0),
|
|
895
|
+
"failed": status.get('failed_count', 0)
|
|
896
|
+
},
|
|
897
|
+
"active_tasks": status.get('active_tasks', []),
|
|
898
|
+
"recent_events": status.get('recent_events', [])[-max_history:]
|
|
899
|
+
},
|
|
900
|
+
"execution_time": time.time() - start_time,
|
|
901
|
+
"timestamp": datetime.now().isoformat()
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
except Exception as e:
|
|
905
|
+
logger.error("[TeamMonitor] Failed", node_id=node_id, error=str(e))
|
|
906
|
+
return {
|
|
907
|
+
"success": False,
|
|
908
|
+
"node_id": node_id,
|
|
909
|
+
"node_type": "teamMonitor",
|
|
910
|
+
"error": str(e),
|
|
911
|
+
"execution_time": time.time() - start_time,
|
|
912
|
+
"timestamp": datetime.now().isoformat()
|
|
913
|
+
}
|
|
@@ -30,12 +30,13 @@ from services.handlers import (
|
|
|
30
30
|
handle_create_map, handle_add_locations, handle_nearby_places,
|
|
31
31
|
handle_text_generator, handle_file_handler,
|
|
32
32
|
handle_chat_send, handle_chat_history,
|
|
33
|
-
handle_start, handle_cron_scheduler, handle_timer, handle_console,
|
|
33
|
+
handle_start, handle_cron_scheduler, handle_timer, handle_console, handle_team_monitor,
|
|
34
34
|
handle_whatsapp_send, handle_whatsapp_db,
|
|
35
35
|
handle_social_receive, handle_social_send,
|
|
36
36
|
handle_http_scraper, handle_file_downloader, handle_document_parser,
|
|
37
37
|
handle_text_chunker, handle_embedding_generator, handle_vector_store,
|
|
38
38
|
handle_task_manager,
|
|
39
|
+
handle_twitter_send, handle_twitter_search, handle_twitter_user,
|
|
39
40
|
)
|
|
40
41
|
|
|
41
42
|
if TYPE_CHECKING:
|
|
@@ -122,6 +123,7 @@ class NodeExecutor:
|
|
|
122
123
|
'consumer_agent': partial(handle_chat_agent, ai_service=self.ai_service, database=self.database),
|
|
123
124
|
'autonomous_agent': partial(handle_chat_agent, ai_service=self.ai_service, database=self.database),
|
|
124
125
|
'orchestrator_agent': partial(handle_chat_agent, ai_service=self.ai_service, database=self.database),
|
|
126
|
+
'ai_employee': partial(handle_chat_agent, ai_service=self.ai_service, database=self.database),
|
|
125
127
|
'simpleMemory': handle_simple_memory,
|
|
126
128
|
# Maps
|
|
127
129
|
'gmaps_create': partial(handle_create_map, maps_service=self.maps_service),
|
|
@@ -133,6 +135,10 @@ class NodeExecutor:
|
|
|
133
135
|
# WhatsApp
|
|
134
136
|
'whatsappSend': handle_whatsapp_send,
|
|
135
137
|
'whatsappDb': handle_whatsapp_db,
|
|
138
|
+
# Twitter/X
|
|
139
|
+
'twitterSend': handle_twitter_send,
|
|
140
|
+
'twitterSearch': handle_twitter_search,
|
|
141
|
+
'twitterUser': handle_twitter_user,
|
|
136
142
|
# Social (unified messaging)
|
|
137
143
|
# Note: socialReceive handled in _dispatch with connected_outputs
|
|
138
144
|
'socialSend': handle_social_send,
|
|
@@ -150,6 +156,8 @@ class NodeExecutor:
|
|
|
150
156
|
'vectorStore': handle_vector_store,
|
|
151
157
|
# Task management
|
|
152
158
|
'taskManager': handle_task_manager,
|
|
159
|
+
# Team monitoring
|
|
160
|
+
'teamMonitor': handle_team_monitor,
|
|
153
161
|
# Note: 'console' handled in _dispatch with connected_outputs
|
|
154
162
|
}
|
|
155
163
|
|
|
@@ -227,6 +235,12 @@ class NodeExecutor:
|
|
|
227
235
|
await self._output_store(session_id, node_id, "output_contact", output_data.get('contact', {}))
|
|
228
236
|
await self._output_store(session_id, node_id, "output_metadata", output_data.get('metadata', {}))
|
|
229
237
|
|
|
238
|
+
# Store with multiple keys for different handle IDs used by frontend components:
|
|
239
|
+
# - output_main: SquareNode, GenericNode, TriggerNode, StartNode
|
|
240
|
+
# - output_top: AIAgentNode (agents use top output handle)
|
|
241
|
+
# - output_0: backward compatibility
|
|
242
|
+
await self._output_store(session_id, node_id, "output_main", output_data)
|
|
243
|
+
await self._output_store(session_id, node_id, "output_top", output_data)
|
|
230
244
|
await self._output_store(session_id, node_id, "output_0", output_data)
|
|
231
245
|
|
|
232
246
|
return result
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""LLM pricing service for token cost calculation.
|
|
2
|
+
|
|
3
|
+
Provides cost calculation for all supported LLM providers based on official pricing.
|
|
4
|
+
Pricing is in USD per million tokens (MTok).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Dict, Optional
|
|
9
|
+
from core.logging import get_logger
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ModelPricing:
|
|
16
|
+
"""Pricing for a specific model."""
|
|
17
|
+
input_per_mtok: float # USD per 1M input tokens
|
|
18
|
+
output_per_mtok: float # USD per 1M output tokens
|
|
19
|
+
cache_read_per_mtok: Optional[float] = None # USD per 1M cache read tokens (Anthropic)
|
|
20
|
+
reasoning_per_mtok: Optional[float] = None # USD per 1M reasoning tokens (OpenAI o-series)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Official pricing as of February 2026
|
|
24
|
+
# Sources: openai.com/api/pricing, anthropic.com/pricing, ai.google.dev/pricing,
|
|
25
|
+
# groq.com/pricing, cerebras.ai/pricing
|
|
26
|
+
PRICING_REGISTRY: Dict[str, Dict[str, ModelPricing]] = {
|
|
27
|
+
'openai': {
|
|
28
|
+
# GPT-5 series
|
|
29
|
+
'gpt-5.2': ModelPricing(1.75, 14.00),
|
|
30
|
+
'gpt-5.1': ModelPricing(1.25, 10.00),
|
|
31
|
+
'gpt-5': ModelPricing(1.25, 10.00),
|
|
32
|
+
'gpt-5-mini': ModelPricing(0.25, 2.00),
|
|
33
|
+
'gpt-5-nano': ModelPricing(0.05, 0.40),
|
|
34
|
+
'gpt-5-pro': ModelPricing(15.00, 120.00),
|
|
35
|
+
# O-series (reasoning models)
|
|
36
|
+
'o3': ModelPricing(2.00, 8.00, reasoning_per_mtok=8.00),
|
|
37
|
+
'o3-mini': ModelPricing(1.10, 4.40, reasoning_per_mtok=4.40),
|
|
38
|
+
'o3-pro': ModelPricing(20.00, 80.00, reasoning_per_mtok=80.00),
|
|
39
|
+
'o4-mini': ModelPricing(1.10, 4.40, reasoning_per_mtok=4.40),
|
|
40
|
+
'o1': ModelPricing(15.00, 60.00, reasoning_per_mtok=60.00),
|
|
41
|
+
'o1-mini': ModelPricing(1.10, 4.40, reasoning_per_mtok=4.40),
|
|
42
|
+
# GPT-4 series
|
|
43
|
+
'gpt-4o': ModelPricing(2.50, 10.00),
|
|
44
|
+
'gpt-4o-mini': ModelPricing(0.15, 0.60),
|
|
45
|
+
'gpt-4.1': ModelPricing(2.00, 8.00),
|
|
46
|
+
'gpt-4.1-mini': ModelPricing(0.40, 1.60),
|
|
47
|
+
'gpt-4.1-nano': ModelPricing(0.10, 0.40),
|
|
48
|
+
'gpt-4-turbo': ModelPricing(10.00, 30.00),
|
|
49
|
+
'gpt-4': ModelPricing(30.00, 60.00),
|
|
50
|
+
# GPT-3.5
|
|
51
|
+
'gpt-3.5-turbo': ModelPricing(0.50, 1.50),
|
|
52
|
+
# Default fallback
|
|
53
|
+
'_default': ModelPricing(2.50, 10.00),
|
|
54
|
+
},
|
|
55
|
+
'anthropic': {
|
|
56
|
+
# Claude 4.x series
|
|
57
|
+
'claude-opus-4.6': ModelPricing(5.00, 25.00, cache_read_per_mtok=0.50),
|
|
58
|
+
'claude-opus-4.5': ModelPricing(5.00, 25.00, cache_read_per_mtok=0.50),
|
|
59
|
+
'claude-opus-4.1': ModelPricing(15.00, 75.00, cache_read_per_mtok=1.50),
|
|
60
|
+
'claude-opus-4': ModelPricing(15.00, 75.00, cache_read_per_mtok=1.50),
|
|
61
|
+
'claude-sonnet-4.6': ModelPricing(3.00, 15.00, cache_read_per_mtok=0.30),
|
|
62
|
+
'claude-sonnet-4.5': ModelPricing(3.00, 15.00, cache_read_per_mtok=0.30),
|
|
63
|
+
'claude-sonnet-4': ModelPricing(3.00, 15.00, cache_read_per_mtok=0.30),
|
|
64
|
+
'claude-haiku-4.5': ModelPricing(1.00, 5.00, cache_read_per_mtok=0.10),
|
|
65
|
+
# Claude 3.x series (legacy)
|
|
66
|
+
'claude-3-opus': ModelPricing(15.00, 75.00, cache_read_per_mtok=1.50),
|
|
67
|
+
'claude-3-5-sonnet': ModelPricing(3.00, 15.00, cache_read_per_mtok=0.30),
|
|
68
|
+
'claude-3-sonnet': ModelPricing(3.00, 15.00, cache_read_per_mtok=0.30),
|
|
69
|
+
'claude-3-haiku': ModelPricing(0.25, 1.25, cache_read_per_mtok=0.03),
|
|
70
|
+
'claude-3.5-haiku': ModelPricing(0.80, 4.00, cache_read_per_mtok=0.08),
|
|
71
|
+
# Partial matches (for model names like claude-3-5-sonnet-20241022)
|
|
72
|
+
'claude-opus': ModelPricing(5.00, 25.00, cache_read_per_mtok=0.50),
|
|
73
|
+
'claude-sonnet': ModelPricing(3.00, 15.00, cache_read_per_mtok=0.30),
|
|
74
|
+
'claude-haiku': ModelPricing(1.00, 5.00, cache_read_per_mtok=0.10),
|
|
75
|
+
# Default fallback
|
|
76
|
+
'_default': ModelPricing(3.00, 15.00, cache_read_per_mtok=0.30),
|
|
77
|
+
},
|
|
78
|
+
'gemini': {
|
|
79
|
+
# Gemini 3.x series
|
|
80
|
+
'gemini-3-pro': ModelPricing(2.00, 12.00),
|
|
81
|
+
'gemini-3-flash': ModelPricing(0.50, 3.00),
|
|
82
|
+
# Gemini 2.5 series
|
|
83
|
+
'gemini-2.5-pro': ModelPricing(1.25, 10.00),
|
|
84
|
+
'gemini-2.5-flash': ModelPricing(0.30, 2.50),
|
|
85
|
+
'gemini-2.5-flash-lite': ModelPricing(0.10, 0.40),
|
|
86
|
+
# Gemini 2.0 series
|
|
87
|
+
'gemini-2.0-flash': ModelPricing(0.10, 0.40),
|
|
88
|
+
'gemini-2.0-flash-lite': ModelPricing(0.08, 0.30),
|
|
89
|
+
# Gemini 1.x series (legacy)
|
|
90
|
+
'gemini-1.5-pro': ModelPricing(1.25, 5.00),
|
|
91
|
+
'gemini-1.5-flash': ModelPricing(0.075, 0.30),
|
|
92
|
+
'gemini-pro': ModelPricing(0.50, 1.50),
|
|
93
|
+
# Default fallback
|
|
94
|
+
'_default': ModelPricing(0.50, 2.50),
|
|
95
|
+
},
|
|
96
|
+
'groq': {
|
|
97
|
+
# Llama 4 series
|
|
98
|
+
'llama-4-scout': ModelPricing(0.11, 0.34),
|
|
99
|
+
'llama-4-maverick': ModelPricing(0.20, 0.60),
|
|
100
|
+
# Llama 3.x series
|
|
101
|
+
'llama-3.3-70b': ModelPricing(0.59, 0.79),
|
|
102
|
+
'llama-3.1-70b': ModelPricing(0.59, 0.79),
|
|
103
|
+
'llama-3.1-8b': ModelPricing(0.05, 0.08),
|
|
104
|
+
'llama3-70b': ModelPricing(0.59, 0.79),
|
|
105
|
+
'llama3-8b': ModelPricing(0.05, 0.08),
|
|
106
|
+
# Qwen series
|
|
107
|
+
'qwen3-32b': ModelPricing(0.29, 0.59),
|
|
108
|
+
'qwen-qwq-32b': ModelPricing(0.29, 0.59),
|
|
109
|
+
# Mixtral
|
|
110
|
+
'mixtral-8x7b': ModelPricing(0.24, 0.24),
|
|
111
|
+
# GPT OSS
|
|
112
|
+
'gpt-oss-20b': ModelPricing(0.075, 0.30),
|
|
113
|
+
'gpt-oss-120b': ModelPricing(0.15, 0.60),
|
|
114
|
+
# Default fallback
|
|
115
|
+
'_default': ModelPricing(0.29, 0.59),
|
|
116
|
+
},
|
|
117
|
+
'cerebras': {
|
|
118
|
+
# Llama series
|
|
119
|
+
'llama-3.1-8b': ModelPricing(0.10, 0.10),
|
|
120
|
+
'llama-3.1-70b': ModelPricing(0.60, 0.60),
|
|
121
|
+
'llama-3.1-405b': ModelPricing(6.00, 12.00),
|
|
122
|
+
'llama3.1-8b': ModelPricing(0.10, 0.10),
|
|
123
|
+
'llama3.1-70b': ModelPricing(0.60, 0.60),
|
|
124
|
+
# Qwen
|
|
125
|
+
'qwen-3-32b': ModelPricing(0.20, 0.20),
|
|
126
|
+
# Default fallback
|
|
127
|
+
'_default': ModelPricing(0.60, 0.60),
|
|
128
|
+
},
|
|
129
|
+
'openrouter': {
|
|
130
|
+
# OpenRouter passes through provider pricing
|
|
131
|
+
# These are approximate defaults; actual cost depends on underlying provider
|
|
132
|
+
'_default': ModelPricing(1.00, 5.00),
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class PricingService:
|
|
138
|
+
"""Service for calculating LLM token costs."""
|
|
139
|
+
|
|
140
|
+
def __init__(self):
|
|
141
|
+
self._registry = PRICING_REGISTRY
|
|
142
|
+
|
|
143
|
+
def get_pricing(self, provider: str, model: str) -> ModelPricing:
|
|
144
|
+
"""Get pricing for a specific model.
|
|
145
|
+
|
|
146
|
+
Uses partial matching: 'claude-3-5-sonnet-20241022' matches 'claude-3-5-sonnet'.
|
|
147
|
+
Falls back to '_default' if no match found.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
provider: Provider name (openai, anthropic, gemini, groq, cerebras, openrouter)
|
|
151
|
+
model: Model name or ID
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
ModelPricing with rates per million tokens
|
|
155
|
+
"""
|
|
156
|
+
provider_lower = provider.lower()
|
|
157
|
+
model_lower = model.lower() if model else ''
|
|
158
|
+
|
|
159
|
+
provider_pricing = self._registry.get(provider_lower, {})
|
|
160
|
+
|
|
161
|
+
# Try exact match first
|
|
162
|
+
if model_lower in provider_pricing:
|
|
163
|
+
return provider_pricing[model_lower]
|
|
164
|
+
|
|
165
|
+
# Try partial match (model name starts with a known key)
|
|
166
|
+
for model_key, pricing in provider_pricing.items():
|
|
167
|
+
if model_key != '_default' and model_lower.startswith(model_key):
|
|
168
|
+
return pricing
|
|
169
|
+
|
|
170
|
+
# Try if any key is contained in the model name
|
|
171
|
+
for model_key, pricing in provider_pricing.items():
|
|
172
|
+
if model_key != '_default' and model_key in model_lower:
|
|
173
|
+
return pricing
|
|
174
|
+
|
|
175
|
+
# Fall back to provider default
|
|
176
|
+
default_pricing = provider_pricing.get('_default')
|
|
177
|
+
if default_pricing:
|
|
178
|
+
return default_pricing
|
|
179
|
+
|
|
180
|
+
# Ultimate fallback
|
|
181
|
+
logger.warning(f"[Pricing] No pricing found for {provider}/{model}, using global default")
|
|
182
|
+
return ModelPricing(1.00, 5.00)
|
|
183
|
+
|
|
184
|
+
def calculate_cost(
|
|
185
|
+
self,
|
|
186
|
+
provider: str,
|
|
187
|
+
model: str,
|
|
188
|
+
input_tokens: int,
|
|
189
|
+
output_tokens: int,
|
|
190
|
+
cache_read_tokens: int = 0,
|
|
191
|
+
cache_creation_tokens: int = 0,
|
|
192
|
+
reasoning_tokens: int = 0
|
|
193
|
+
) -> Dict[str, float]:
|
|
194
|
+
"""Calculate cost for token usage.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
provider: LLM provider name
|
|
198
|
+
model: Model name/ID
|
|
199
|
+
input_tokens: Number of input tokens
|
|
200
|
+
output_tokens: Number of output tokens
|
|
201
|
+
cache_read_tokens: Number of cache read tokens (Anthropic)
|
|
202
|
+
cache_creation_tokens: Number of cache creation tokens (Anthropic)
|
|
203
|
+
reasoning_tokens: Number of reasoning tokens (OpenAI o-series)
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Dict with cost breakdown:
|
|
207
|
+
- input_cost: USD for input tokens
|
|
208
|
+
- output_cost: USD for output tokens
|
|
209
|
+
- cache_cost: USD for cache tokens
|
|
210
|
+
- reasoning_cost: USD for reasoning tokens
|
|
211
|
+
- total_cost: Total USD cost
|
|
212
|
+
"""
|
|
213
|
+
pricing = self.get_pricing(provider, model)
|
|
214
|
+
|
|
215
|
+
# Calculate costs (prices are per 1M tokens)
|
|
216
|
+
input_cost = (input_tokens / 1_000_000) * pricing.input_per_mtok
|
|
217
|
+
output_cost = (output_tokens / 1_000_000) * pricing.output_per_mtok
|
|
218
|
+
|
|
219
|
+
# Cache costs (Anthropic pattern)
|
|
220
|
+
cache_cost = 0.0
|
|
221
|
+
if pricing.cache_read_per_mtok:
|
|
222
|
+
# Cache reads are discounted (typically 10% of input price)
|
|
223
|
+
cache_cost += (cache_read_tokens / 1_000_000) * pricing.cache_read_per_mtok
|
|
224
|
+
# Cache creation is charged at 1.25x output rate
|
|
225
|
+
cache_cost += (cache_creation_tokens / 1_000_000) * pricing.output_per_mtok * 1.25
|
|
226
|
+
|
|
227
|
+
# Reasoning costs (OpenAI o-series)
|
|
228
|
+
reasoning_cost = 0.0
|
|
229
|
+
if pricing.reasoning_per_mtok and reasoning_tokens > 0:
|
|
230
|
+
reasoning_cost = (reasoning_tokens / 1_000_000) * pricing.reasoning_per_mtok
|
|
231
|
+
|
|
232
|
+
total_cost = input_cost + output_cost + cache_cost + reasoning_cost
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
'input_cost': round(input_cost, 6),
|
|
236
|
+
'output_cost': round(output_cost, 6),
|
|
237
|
+
'cache_cost': round(cache_cost, 6),
|
|
238
|
+
'reasoning_cost': round(reasoning_cost, 6),
|
|
239
|
+
'total_cost': round(total_cost, 6),
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
def get_all_pricing(self) -> Dict[str, Dict[str, Dict[str, float]]]:
|
|
243
|
+
"""Get all pricing data for frontend display.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Nested dict: {provider: {model: {input, output, cache_read, reasoning}}}
|
|
247
|
+
"""
|
|
248
|
+
result = {}
|
|
249
|
+
for provider, models in self._registry.items():
|
|
250
|
+
result[provider] = {}
|
|
251
|
+
for model, pricing in models.items():
|
|
252
|
+
result[provider][model] = {
|
|
253
|
+
'input': pricing.input_per_mtok,
|
|
254
|
+
'output': pricing.output_per_mtok,
|
|
255
|
+
'cache_read': pricing.cache_read_per_mtok,
|
|
256
|
+
'reasoning': pricing.reasoning_per_mtok,
|
|
257
|
+
}
|
|
258
|
+
return result
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# Singleton instance
|
|
262
|
+
_service: Optional[PricingService] = None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def get_pricing_service() -> PricingService:
|
|
266
|
+
"""Get the singleton PricingService instance."""
|
|
267
|
+
global _service
|
|
268
|
+
if _service is None:
|
|
269
|
+
_service = PricingService()
|
|
270
|
+
return _service
|
|
@@ -41,6 +41,13 @@ class StatusBroadcaster:
|
|
|
41
41
|
"device_id": None,
|
|
42
42
|
"qr": None
|
|
43
43
|
},
|
|
44
|
+
"twitter": {
|
|
45
|
+
"connected": False,
|
|
46
|
+
"username": None,
|
|
47
|
+
"user_id": None,
|
|
48
|
+
"name": None,
|
|
49
|
+
"profile_image_url": None
|
|
50
|
+
},
|
|
44
51
|
"api_keys": {}, # provider -> validation status
|
|
45
52
|
"nodes": {}, # node_id -> node status
|
|
46
53
|
"variables": {}, # variable_name -> value
|
|
@@ -73,6 +80,9 @@ class StatusBroadcaster:
|
|
|
73
80
|
# This ensures client sees actual connection state (especially after auto-connect)
|
|
74
81
|
await self._refresh_whatsapp_status()
|
|
75
82
|
|
|
83
|
+
# Fetch Twitter status from stored OAuth tokens
|
|
84
|
+
await self._refresh_twitter_status()
|
|
85
|
+
|
|
76
86
|
# Auto-reconnect Android relay if there's a stored session
|
|
77
87
|
await self._auto_reconnect_android_relay()
|
|
78
88
|
|
|
@@ -228,6 +238,56 @@ class StatusBroadcaster:
|
|
|
228
238
|
# Don't fail client connection if WhatsApp service is down
|
|
229
239
|
logger.debug(f"[StatusBroadcaster] Could not refresh WhatsApp status: {e}")
|
|
230
240
|
|
|
241
|
+
async def _refresh_twitter_status(self):
|
|
242
|
+
"""Fetch Twitter status from stored OAuth tokens in database.
|
|
243
|
+
|
|
244
|
+
Called on client connect to check if user is authenticated with Twitter.
|
|
245
|
+
"""
|
|
246
|
+
try:
|
|
247
|
+
from core.container import container
|
|
248
|
+
auth_service = container.auth_service()
|
|
249
|
+
|
|
250
|
+
# Check for stored access token
|
|
251
|
+
access_token = await auth_service.get_api_key("twitter_access_token")
|
|
252
|
+
if not access_token:
|
|
253
|
+
self._status["twitter"] = {
|
|
254
|
+
"connected": False,
|
|
255
|
+
"username": None,
|
|
256
|
+
"user_id": None,
|
|
257
|
+
"name": None,
|
|
258
|
+
"profile_image_url": None
|
|
259
|
+
}
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
# Get stored user info
|
|
263
|
+
user_info_str = await auth_service.get_api_key("twitter_user_info")
|
|
264
|
+
if user_info_str:
|
|
265
|
+
# Format: "user_id:username:name"
|
|
266
|
+
parts = user_info_str.split(":", 2)
|
|
267
|
+
user_id = parts[0] if len(parts) > 0 else None
|
|
268
|
+
username = parts[1] if len(parts) > 1 else None
|
|
269
|
+
name = parts[2] if len(parts) > 2 else None
|
|
270
|
+
|
|
271
|
+
self._status["twitter"] = {
|
|
272
|
+
"connected": True,
|
|
273
|
+
"username": username,
|
|
274
|
+
"user_id": user_id,
|
|
275
|
+
"name": name,
|
|
276
|
+
"profile_image_url": None # Could fetch fresh if needed
|
|
277
|
+
}
|
|
278
|
+
logger.debug(f"[StatusBroadcaster] Twitter status: connected as @{username}")
|
|
279
|
+
else:
|
|
280
|
+
# Has token but no user info - still connected
|
|
281
|
+
self._status["twitter"] = {
|
|
282
|
+
"connected": True,
|
|
283
|
+
"username": None,
|
|
284
|
+
"user_id": None,
|
|
285
|
+
"name": None,
|
|
286
|
+
"profile_image_url": None
|
|
287
|
+
}
|
|
288
|
+
except Exception as e:
|
|
289
|
+
logger.debug(f"[StatusBroadcaster] Could not refresh Twitter status: {e}")
|
|
290
|
+
|
|
231
291
|
async def _auto_reconnect_android_relay(self):
|
|
232
292
|
"""Auto-reconnect to Android relay if there's a stored pairing session.
|
|
233
293
|
|
|
@@ -755,6 +815,25 @@ class StatusBroadcaster:
|
|
|
755
815
|
"type": "terminal_logs_cleared"
|
|
756
816
|
})
|
|
757
817
|
|
|
818
|
+
# =========================================================================
|
|
819
|
+
# Agent Team Updates
|
|
820
|
+
# =========================================================================
|
|
821
|
+
|
|
822
|
+
async def broadcast_team_event(self, team_id: str, event_type: str, data: Dict[str, Any]):
|
|
823
|
+
"""Broadcast a team-related event to all connected clients.
|
|
824
|
+
|
|
825
|
+
Args:
|
|
826
|
+
team_id: The team ID
|
|
827
|
+
event_type: Event type (team_created, task_added, task_claimed, etc.)
|
|
828
|
+
data: Event data
|
|
829
|
+
"""
|
|
830
|
+
await self.broadcast({
|
|
831
|
+
"type": "team_event",
|
|
832
|
+
"team_id": team_id,
|
|
833
|
+
"event_type": event_type,
|
|
834
|
+
"data": data
|
|
835
|
+
})
|
|
836
|
+
|
|
758
837
|
# =========================================================================
|
|
759
838
|
# Generic Updates
|
|
760
839
|
# =========================================================================
|