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
|
@@ -10,6 +10,7 @@ from datetime import datetime, timezone
|
|
|
10
10
|
from typing import Dict, Any, Optional, TYPE_CHECKING
|
|
11
11
|
|
|
12
12
|
from core.logging import get_logger
|
|
13
|
+
from services.pricing import get_pricing_service
|
|
13
14
|
|
|
14
15
|
if TYPE_CHECKING:
|
|
15
16
|
from core.database import Database
|
|
@@ -92,20 +93,54 @@ class CompactionService:
|
|
|
92
93
|
return {"context_management": {"compact_threshold": threshold or self._config.threshold}}
|
|
93
94
|
|
|
94
95
|
async def track(self, session_id: str, node_id: str, provider: str, model: str, usage: Dict[str, int]) -> Dict[str, Any]:
|
|
95
|
-
"""Track token usage and return compaction status."""
|
|
96
|
-
|
|
96
|
+
"""Track token usage and cost, return compaction status."""
|
|
97
|
+
# Calculate cost using PricingService
|
|
98
|
+
pricing_service = get_pricing_service()
|
|
99
|
+
cost = pricing_service.calculate_cost(
|
|
100
|
+
provider=provider,
|
|
101
|
+
model=model,
|
|
102
|
+
input_tokens=usage.get("input_tokens", 0),
|
|
103
|
+
output_tokens=usage.get("output_tokens", 0),
|
|
104
|
+
cache_read_tokens=usage.get("cache_read_tokens", 0),
|
|
105
|
+
cache_creation_tokens=usage.get("cache_creation_tokens", 0),
|
|
106
|
+
reasoning_tokens=usage.get("reasoning_tokens", 0)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Save token metric with cost fields
|
|
110
|
+
await self._db.save_token_metric({
|
|
111
|
+
"session_id": session_id,
|
|
112
|
+
"node_id": node_id,
|
|
113
|
+
"provider": provider,
|
|
114
|
+
"model": model,
|
|
115
|
+
**usage,
|
|
116
|
+
"input_cost": cost["input_cost"],
|
|
117
|
+
"output_cost": cost["output_cost"],
|
|
118
|
+
"cache_cost": cost["cache_cost"],
|
|
119
|
+
"total_cost": cost["total_cost"]
|
|
120
|
+
})
|
|
97
121
|
|
|
98
122
|
state = await self._db.get_or_create_session_token_state(session_id)
|
|
99
123
|
new_total = state["cumulative_total"] + usage.get("total_tokens", 0)
|
|
124
|
+
new_total_cost = state.get("cumulative_total_cost", 0.0) + cost["total_cost"]
|
|
100
125
|
|
|
126
|
+
# Update cumulative state with cost
|
|
101
127
|
await self._db.update_session_token_state(session_id, {
|
|
102
128
|
"cumulative_input_tokens": state["cumulative_input_tokens"] + usage.get("input_tokens", 0),
|
|
103
129
|
"cumulative_output_tokens": state["cumulative_output_tokens"] + usage.get("output_tokens", 0),
|
|
104
|
-
"cumulative_total": new_total
|
|
130
|
+
"cumulative_total": new_total,
|
|
131
|
+
"cumulative_input_cost": state.get("cumulative_input_cost", 0.0) + cost["input_cost"],
|
|
132
|
+
"cumulative_output_cost": state.get("cumulative_output_cost", 0.0) + cost["output_cost"],
|
|
133
|
+
"cumulative_total_cost": new_total_cost
|
|
105
134
|
})
|
|
106
135
|
|
|
107
136
|
threshold = state.get("custom_threshold") or self._config.threshold
|
|
108
|
-
return {
|
|
137
|
+
return {
|
|
138
|
+
"total": new_total,
|
|
139
|
+
"total_cost": new_total_cost,
|
|
140
|
+
"cost": cost,
|
|
141
|
+
"threshold": threshold,
|
|
142
|
+
"needs_compaction": self._config.enabled and new_total >= threshold
|
|
143
|
+
}
|
|
109
144
|
|
|
110
145
|
async def record(self, session_id: str, node_id: str, provider: str, model: str, tokens_before: int, tokens_after: int, summary: Optional[str] = None) -> None:
|
|
111
146
|
"""Record compaction event after native API handles it."""
|
|
@@ -110,6 +110,11 @@ TRIGGER_REGISTRY: Dict[str, TriggerConfig] = {
|
|
|
110
110
|
event_type='task_completed',
|
|
111
111
|
display_name='Task Completed'
|
|
112
112
|
),
|
|
113
|
+
'twitterReceive': TriggerConfig(
|
|
114
|
+
node_type='twitterReceive',
|
|
115
|
+
event_type='twitter_event_received',
|
|
116
|
+
display_name='Twitter Event'
|
|
117
|
+
),
|
|
113
118
|
# Future triggers - just add to registry:
|
|
114
119
|
# 'emailTrigger': TriggerConfig('emailTrigger', 'email_received', 'Email'),
|
|
115
120
|
# 'mqttTrigger': TriggerConfig('mqttTrigger', 'mqtt_message', 'MQTT Message'),
|
|
@@ -336,12 +341,48 @@ def build_task_completed_filter(params: Dict) -> Callable[[Dict], bool]:
|
|
|
336
341
|
return matches
|
|
337
342
|
|
|
338
343
|
|
|
344
|
+
def build_twitter_filter(params: Dict) -> Callable[[Dict], bool]:
|
|
345
|
+
"""Build filter function for Twitter events.
|
|
346
|
+
|
|
347
|
+
Filters by:
|
|
348
|
+
- trigger_type: 'mentions', 'search', 'user_timeline'
|
|
349
|
+
- search_query: Search query for 'search' trigger type
|
|
350
|
+
- user_id: User ID for 'user_timeline' trigger type
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
params: Node parameters
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
Filter function that checks if event matches criteria
|
|
357
|
+
"""
|
|
358
|
+
trigger_type = params.get('trigger_type', 'mentions')
|
|
359
|
+
search_query = params.get('search_query', '')
|
|
360
|
+
user_id = params.get('user_id', '')
|
|
361
|
+
|
|
362
|
+
def matches(data: Dict) -> bool:
|
|
363
|
+
event_type = data.get('trigger_type', '')
|
|
364
|
+
if trigger_type != 'all' and event_type != trigger_type:
|
|
365
|
+
return False
|
|
366
|
+
if trigger_type == 'search' and search_query:
|
|
367
|
+
# Check if search query matches
|
|
368
|
+
event_query = data.get('query', '')
|
|
369
|
+
if search_query.lower() not in event_query.lower():
|
|
370
|
+
return False
|
|
371
|
+
if trigger_type == 'user_timeline' and user_id:
|
|
372
|
+
if data.get('user_id') != user_id:
|
|
373
|
+
return False
|
|
374
|
+
return True
|
|
375
|
+
|
|
376
|
+
return matches
|
|
377
|
+
|
|
378
|
+
|
|
339
379
|
# Registry of filter builders per trigger type
|
|
340
380
|
FILTER_BUILDERS: Dict[str, Callable[[Dict], Callable[[Dict], bool]]] = {
|
|
341
381
|
'whatsappReceive': build_whatsapp_filter,
|
|
342
382
|
'webhookTrigger': build_webhook_filter,
|
|
343
383
|
'chatTrigger': build_chat_filter,
|
|
344
384
|
'taskTrigger': build_task_completed_filter,
|
|
385
|
+
'twitterReceive': build_twitter_filter,
|
|
345
386
|
}
|
|
346
387
|
|
|
347
388
|
|
|
@@ -57,6 +57,7 @@ from .utility import (
|
|
|
57
57
|
handle_cron_scheduler,
|
|
58
58
|
handle_timer,
|
|
59
59
|
handle_console,
|
|
60
|
+
handle_team_monitor,
|
|
60
61
|
)
|
|
61
62
|
|
|
62
63
|
# WhatsApp handlers
|
|
@@ -65,6 +66,13 @@ from .whatsapp import (
|
|
|
65
66
|
handle_whatsapp_db,
|
|
66
67
|
)
|
|
67
68
|
|
|
69
|
+
# Twitter handlers
|
|
70
|
+
from .twitter import (
|
|
71
|
+
handle_twitter_send,
|
|
72
|
+
handle_twitter_search,
|
|
73
|
+
handle_twitter_user,
|
|
74
|
+
)
|
|
75
|
+
|
|
68
76
|
# Social handlers (unified messaging)
|
|
69
77
|
from .social import (
|
|
70
78
|
handle_social_receive,
|
|
@@ -115,9 +123,14 @@ __all__ = [
|
|
|
115
123
|
'handle_cron_scheduler',
|
|
116
124
|
'handle_timer',
|
|
117
125
|
'handle_console',
|
|
126
|
+
'handle_team_monitor',
|
|
118
127
|
# WhatsApp
|
|
119
128
|
'handle_whatsapp_send',
|
|
120
129
|
'handle_whatsapp_db',
|
|
130
|
+
# Twitter
|
|
131
|
+
'handle_twitter_send',
|
|
132
|
+
'handle_twitter_search',
|
|
133
|
+
'handle_twitter_user',
|
|
121
134
|
# Social
|
|
122
135
|
'handle_social_receive',
|
|
123
136
|
'handle_social_send',
|
|
@@ -10,6 +10,9 @@ if TYPE_CHECKING:
|
|
|
10
10
|
|
|
11
11
|
logger = get_logger(__name__)
|
|
12
12
|
|
|
13
|
+
# Team lead types that can create teams
|
|
14
|
+
TEAM_LEAD_TYPES = {'orchestrator_agent', 'ai_employee'}
|
|
15
|
+
|
|
13
16
|
|
|
14
17
|
async def _collect_agent_connections(
|
|
15
18
|
node_id: str,
|
|
@@ -302,6 +305,50 @@ async def _collect_agent_connections(
|
|
|
302
305
|
return memory_data, skill_data, tool_data, input_data, task_data
|
|
303
306
|
|
|
304
307
|
|
|
308
|
+
async def _collect_teammate_connections(
|
|
309
|
+
node_id: str,
|
|
310
|
+
context: Dict[str, Any],
|
|
311
|
+
database: "Database"
|
|
312
|
+
) -> List[Dict[str, Any]]:
|
|
313
|
+
"""Collect agents connected via input-teammates handle for team mode.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
node_id: The team lead node ID
|
|
317
|
+
context: Execution context with nodes, edges
|
|
318
|
+
database: Database instance
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
List of teammate node info dicts
|
|
322
|
+
"""
|
|
323
|
+
nodes = context.get('nodes', [])
|
|
324
|
+
edges = context.get('edges', [])
|
|
325
|
+
teammates = []
|
|
326
|
+
|
|
327
|
+
for edge in edges:
|
|
328
|
+
if edge.get('target') != node_id or edge.get('targetHandle') != 'input-teammates':
|
|
329
|
+
continue
|
|
330
|
+
|
|
331
|
+
source_id = edge.get('source')
|
|
332
|
+
source_node = next((n for n in nodes if n.get('id') == source_id), None)
|
|
333
|
+
if not source_node:
|
|
334
|
+
continue
|
|
335
|
+
|
|
336
|
+
node_type = source_node.get('type', '')
|
|
337
|
+
if node_type not in AI_AGENT_TYPES:
|
|
338
|
+
continue
|
|
339
|
+
|
|
340
|
+
params = await database.get_node_parameters(source_id) or {}
|
|
341
|
+
teammates.append({
|
|
342
|
+
'node_id': source_id,
|
|
343
|
+
'node_type': node_type,
|
|
344
|
+
'label': source_node.get('data', {}).get('label', node_type),
|
|
345
|
+
'parameters': params
|
|
346
|
+
})
|
|
347
|
+
logger.debug(f"[Teams] Found teammate: {node_type} ({source_id})")
|
|
348
|
+
|
|
349
|
+
return teammates
|
|
350
|
+
|
|
351
|
+
|
|
305
352
|
def _format_task_context(task_data: Dict[str, Any]) -> str:
|
|
306
353
|
"""Format task completion data as context for the agent.
|
|
307
354
|
|
|
@@ -485,7 +532,24 @@ async def handle_chat_agent(
|
|
|
485
532
|
from services.status_broadcaster import get_status_broadcaster
|
|
486
533
|
broadcaster = get_status_broadcaster()
|
|
487
534
|
|
|
488
|
-
#
|
|
535
|
+
# Team mode detection for orchestrator_agent and ai_employee nodes
|
|
536
|
+
# Teammates connected via input-teammates become delegation tools
|
|
537
|
+
if node_type in TEAM_LEAD_TYPES:
|
|
538
|
+
teammates = await _collect_teammate_connections(node_id, context, database)
|
|
539
|
+
|
|
540
|
+
if teammates:
|
|
541
|
+
# Add teammates as delegation tools (they become delegate_to_* tools)
|
|
542
|
+
tool_data = tool_data or []
|
|
543
|
+
for tm in teammates:
|
|
544
|
+
tool_data.append({
|
|
545
|
+
'node_id': tm['node_id'],
|
|
546
|
+
'node_type': tm['node_type'],
|
|
547
|
+
'label': tm['label'],
|
|
548
|
+
'parameters': tm.get('parameters', {}),
|
|
549
|
+
})
|
|
550
|
+
logger.info(f"[Teams] Added {len(teammates)} teammates as delegation tools")
|
|
551
|
+
|
|
552
|
+
# Standard execution (no team mode)
|
|
489
553
|
return await ai_service.execute_chat_agent(
|
|
490
554
|
node_id,
|
|
491
555
|
parameters,
|
|
@@ -494,7 +558,7 @@ async def handle_chat_agent(
|
|
|
494
558
|
tool_data=tool_data if tool_data else None,
|
|
495
559
|
broadcaster=broadcaster,
|
|
496
560
|
workflow_id=workflow_id,
|
|
497
|
-
context=context
|
|
561
|
+
context=context
|
|
498
562
|
)
|
|
499
563
|
|
|
500
564
|
|
|
@@ -107,6 +107,18 @@ async def execute_tool(tool_name: str, tool_args: Dict[str, Any],
|
|
|
107
107
|
if node_type == 'whatsappDb':
|
|
108
108
|
return await _execute_whatsapp_db(tool_args, config.get('parameters', {}))
|
|
109
109
|
|
|
110
|
+
# Twitter Send (dual-purpose: workflow node + AI tool)
|
|
111
|
+
if node_type == 'twitterSend':
|
|
112
|
+
return await _execute_twitter_send(tool_args, config.get('parameters', {}))
|
|
113
|
+
|
|
114
|
+
# Twitter Search (dual-purpose: workflow node + AI tool)
|
|
115
|
+
if node_type == 'twitterSearch':
|
|
116
|
+
return await _execute_twitter_search(tool_args, config.get('parameters', {}))
|
|
117
|
+
|
|
118
|
+
# Twitter User (dual-purpose: workflow node + AI tool)
|
|
119
|
+
if node_type == 'twitterUser':
|
|
120
|
+
return await _execute_twitter_user(tool_args, config.get('parameters', {}))
|
|
121
|
+
|
|
110
122
|
# Android toolkit - routes to connected service nodes
|
|
111
123
|
if node_type == 'androidTool':
|
|
112
124
|
return await _execute_android_toolkit(tool_args, config)
|
|
@@ -1118,6 +1130,78 @@ async def _execute_nearby_places(args: Dict[str, Any],
|
|
|
1118
1130
|
return {"error": f"Nearby places search failed: {str(e)}"}
|
|
1119
1131
|
|
|
1120
1132
|
|
|
1133
|
+
async def _execute_twitter_send(args: Dict[str, Any],
|
|
1134
|
+
node_params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1135
|
+
"""Execute Twitter send action via XDK SDK.
|
|
1136
|
+
|
|
1137
|
+
Args:
|
|
1138
|
+
args: LLM-provided arguments (action, text, tweet_id, reply_to_id)
|
|
1139
|
+
node_params: Node parameters
|
|
1140
|
+
|
|
1141
|
+
Returns:
|
|
1142
|
+
Twitter API result
|
|
1143
|
+
"""
|
|
1144
|
+
from services.handlers.twitter import handle_twitter_send
|
|
1145
|
+
|
|
1146
|
+
parameters = {**node_params, **args}
|
|
1147
|
+
# Handle reply_to_id alias
|
|
1148
|
+
if args.get('reply_to_id') and not args.get('tweet_id'):
|
|
1149
|
+
parameters['tweet_id'] = args['reply_to_id']
|
|
1150
|
+
|
|
1151
|
+
return await handle_twitter_send(
|
|
1152
|
+
node_id="tool_twitter_send",
|
|
1153
|
+
node_type="twitterSend",
|
|
1154
|
+
parameters=parameters,
|
|
1155
|
+
context={}
|
|
1156
|
+
)
|
|
1157
|
+
|
|
1158
|
+
|
|
1159
|
+
async def _execute_twitter_search(args: Dict[str, Any],
|
|
1160
|
+
node_params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1161
|
+
"""Execute Twitter search via XDK SDK.
|
|
1162
|
+
|
|
1163
|
+
Args:
|
|
1164
|
+
args: LLM-provided arguments (query, max_results)
|
|
1165
|
+
node_params: Node parameters
|
|
1166
|
+
|
|
1167
|
+
Returns:
|
|
1168
|
+
Search results
|
|
1169
|
+
"""
|
|
1170
|
+
from services.handlers.twitter import handle_twitter_search
|
|
1171
|
+
|
|
1172
|
+
parameters = {**node_params, **args}
|
|
1173
|
+
|
|
1174
|
+
return await handle_twitter_search(
|
|
1175
|
+
node_id="tool_twitter_search",
|
|
1176
|
+
node_type="twitterSearch",
|
|
1177
|
+
parameters=parameters,
|
|
1178
|
+
context={}
|
|
1179
|
+
)
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
async def _execute_twitter_user(args: Dict[str, Any],
|
|
1183
|
+
node_params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1184
|
+
"""Execute Twitter user lookup via XDK SDK.
|
|
1185
|
+
|
|
1186
|
+
Args:
|
|
1187
|
+
args: LLM-provided arguments (operation, username, user_id, max_results)
|
|
1188
|
+
node_params: Node parameters
|
|
1189
|
+
|
|
1190
|
+
Returns:
|
|
1191
|
+
User data
|
|
1192
|
+
"""
|
|
1193
|
+
from services.handlers.twitter import handle_twitter_user
|
|
1194
|
+
|
|
1195
|
+
parameters = {**node_params, **args}
|
|
1196
|
+
|
|
1197
|
+
return await handle_twitter_user(
|
|
1198
|
+
node_id="tool_twitter_user",
|
|
1199
|
+
node_type="twitterUser",
|
|
1200
|
+
parameters=parameters,
|
|
1201
|
+
context={}
|
|
1202
|
+
)
|
|
1203
|
+
|
|
1204
|
+
|
|
1121
1205
|
async def _execute_generic(args: Dict[str, Any],
|
|
1122
1206
|
config: Dict[str, Any]) -> Dict[str, Any]:
|
|
1123
1207
|
"""Execute a generic tool (fallback handler).
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""Twitter/X node handlers using official XDK SDK."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Dict, Any
|
|
5
|
+
from xdk import Client
|
|
6
|
+
|
|
7
|
+
from core.logging import get_logger
|
|
8
|
+
|
|
9
|
+
logger = get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def _get_twitter_client() -> Client:
|
|
13
|
+
"""Get authenticated Twitter client from stored OAuth 2.0 credentials."""
|
|
14
|
+
# Import inside function to avoid circular import
|
|
15
|
+
from core.container import container
|
|
16
|
+
auth_service = container.auth_service()
|
|
17
|
+
access_token = await auth_service.get_api_key("twitter_access_token")
|
|
18
|
+
if not access_token:
|
|
19
|
+
raise ValueError("Twitter not connected. Please authenticate via Credentials.")
|
|
20
|
+
# Use access_token for OAuth 2.0 user token (not bearer_token which is app-only)
|
|
21
|
+
return Client(access_token=access_token)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def _get_my_user_id(client: Client) -> str:
|
|
25
|
+
"""Get the authenticated user's ID."""
|
|
26
|
+
response = client.users.get_me()
|
|
27
|
+
# response.data is a dict with 'id', 'username', 'name' keys
|
|
28
|
+
return response.data["id"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def handle_twitter_send(
|
|
32
|
+
node_id: str,
|
|
33
|
+
node_type: str,
|
|
34
|
+
parameters: Dict[str, Any],
|
|
35
|
+
context: Dict[str, Any]
|
|
36
|
+
) -> Dict[str, Any]:
|
|
37
|
+
"""Handle Twitter send actions: tweet, reply, retweet, like, unlike, delete."""
|
|
38
|
+
start_time = time.time()
|
|
39
|
+
action = parameters.get('action', 'tweet')
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
client = await _get_twitter_client()
|
|
43
|
+
|
|
44
|
+
match action:
|
|
45
|
+
case 'tweet':
|
|
46
|
+
text = parameters.get('text', '')
|
|
47
|
+
if not text:
|
|
48
|
+
raise ValueError("Tweet text is required")
|
|
49
|
+
# XDK API: client.posts.create(body={"text": "..."})
|
|
50
|
+
payload = {"text": text[:280]}
|
|
51
|
+
result = client.posts.create(body=payload)
|
|
52
|
+
return _success(_format_response(result), "tweet_sent", start_time)
|
|
53
|
+
|
|
54
|
+
case 'reply':
|
|
55
|
+
text = parameters.get('text', '')
|
|
56
|
+
reply_to = parameters.get('reply_to_id')
|
|
57
|
+
if not text or not reply_to:
|
|
58
|
+
raise ValueError("Text and reply_to_id are required")
|
|
59
|
+
# XDK API: reply uses nested reply object
|
|
60
|
+
payload = {
|
|
61
|
+
"text": text[:280],
|
|
62
|
+
"reply": {"in_reply_to_tweet_id": reply_to}
|
|
63
|
+
}
|
|
64
|
+
result = client.posts.create(body=payload)
|
|
65
|
+
return _success(_format_response(result), "reply_sent", start_time)
|
|
66
|
+
|
|
67
|
+
case 'retweet':
|
|
68
|
+
tweet_id = parameters.get('tweet_id')
|
|
69
|
+
if not tweet_id:
|
|
70
|
+
raise ValueError("tweet_id is required")
|
|
71
|
+
user_id = await _get_my_user_id(client)
|
|
72
|
+
# XDK API: client.users.repost_post(user_id, body={"tweet_id": "..."})
|
|
73
|
+
payload = {"tweet_id": tweet_id}
|
|
74
|
+
result = client.users.repost_post(user_id, body=payload)
|
|
75
|
+
return _success(_format_response(result), "retweeted", start_time)
|
|
76
|
+
|
|
77
|
+
case 'like':
|
|
78
|
+
tweet_id = parameters.get('tweet_id')
|
|
79
|
+
if not tweet_id:
|
|
80
|
+
raise ValueError("tweet_id is required")
|
|
81
|
+
user_id = await _get_my_user_id(client)
|
|
82
|
+
# XDK API: client.users.like_post(user_id, body={"tweet_id": "..."})
|
|
83
|
+
payload = {"tweet_id": tweet_id}
|
|
84
|
+
result = client.users.like_post(user_id, body=payload)
|
|
85
|
+
return _success(_format_response(result), "liked", start_time)
|
|
86
|
+
|
|
87
|
+
case 'unlike':
|
|
88
|
+
tweet_id = parameters.get('tweet_id')
|
|
89
|
+
if not tweet_id:
|
|
90
|
+
raise ValueError("tweet_id is required")
|
|
91
|
+
user_id = await _get_my_user_id(client)
|
|
92
|
+
# XDK API: client.users.unlike_post(user_id, tweet_id=post_id)
|
|
93
|
+
result = client.users.unlike_post(user_id, tweet_id=tweet_id)
|
|
94
|
+
return _success(_format_response(result), "unliked", start_time)
|
|
95
|
+
|
|
96
|
+
case 'delete':
|
|
97
|
+
tweet_id = parameters.get('tweet_id')
|
|
98
|
+
if not tweet_id:
|
|
99
|
+
raise ValueError("tweet_id is required")
|
|
100
|
+
# XDK API: client.posts.delete(post_id)
|
|
101
|
+
result = client.posts.delete(tweet_id)
|
|
102
|
+
return _success(_format_response(result), "deleted", start_time)
|
|
103
|
+
|
|
104
|
+
case _:
|
|
105
|
+
raise ValueError(f"Unknown action: {action}")
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.error(f"Twitter send error: {e}")
|
|
109
|
+
return {"success": False, "error": str(e), "execution_time": time.time() - start_time}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def handle_twitter_search(
|
|
113
|
+
node_id: str,
|
|
114
|
+
node_type: str,
|
|
115
|
+
parameters: Dict[str, Any],
|
|
116
|
+
context: Dict[str, Any]
|
|
117
|
+
) -> Dict[str, Any]:
|
|
118
|
+
"""Handle Twitter search operations."""
|
|
119
|
+
start_time = time.time()
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
client = await _get_twitter_client()
|
|
123
|
+
query = parameters.get('query', '')
|
|
124
|
+
max_results = min(parameters.get('max_results', 10), 100)
|
|
125
|
+
|
|
126
|
+
if not query:
|
|
127
|
+
raise ValueError("Search query is required")
|
|
128
|
+
|
|
129
|
+
tweets = []
|
|
130
|
+
# XDK API: client.posts.search_recent(query=..., max_results=..., tweet_fields=[...])
|
|
131
|
+
for page in client.posts.search_recent(
|
|
132
|
+
query=query,
|
|
133
|
+
max_results=max_results,
|
|
134
|
+
tweet_fields=["author_id", "created_at"]
|
|
135
|
+
):
|
|
136
|
+
page_data = getattr(page, 'data', []) or []
|
|
137
|
+
tweets.extend([_format_tweet(t) for t in page_data])
|
|
138
|
+
break # Only first page
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
"success": True,
|
|
142
|
+
"result": {"tweets": tweets, "count": len(tweets), "query": query},
|
|
143
|
+
"execution_time": time.time() - start_time
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error(f"Twitter search error: {e}")
|
|
148
|
+
return {"success": False, "error": str(e), "execution_time": time.time() - start_time}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
async def handle_twitter_user(
|
|
152
|
+
node_id: str,
|
|
153
|
+
node_type: str,
|
|
154
|
+
parameters: Dict[str, Any],
|
|
155
|
+
context: Dict[str, Any]
|
|
156
|
+
) -> Dict[str, Any]:
|
|
157
|
+
"""Handle Twitter user lookup operations."""
|
|
158
|
+
start_time = time.time()
|
|
159
|
+
operation = parameters.get('operation', 'me')
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
client = await _get_twitter_client()
|
|
163
|
+
|
|
164
|
+
match operation:
|
|
165
|
+
case 'me':
|
|
166
|
+
# XDK API: client.users.get_me(user_fields=[...])
|
|
167
|
+
result = client.users.get_me(user_fields=["created_at", "description"])
|
|
168
|
+
return _success(_format_user_data(result.data), "user", start_time)
|
|
169
|
+
|
|
170
|
+
case 'by_username':
|
|
171
|
+
username = parameters.get('username')
|
|
172
|
+
if not username:
|
|
173
|
+
raise ValueError("Username is required")
|
|
174
|
+
# XDK API: client.users.get_by_usernames(usernames=[...], user_fields=[...])
|
|
175
|
+
result = client.users.get_by_usernames(
|
|
176
|
+
usernames=[username],
|
|
177
|
+
user_fields=["description", "created_at"]
|
|
178
|
+
)
|
|
179
|
+
users = getattr(result, 'data', []) or []
|
|
180
|
+
if not users:
|
|
181
|
+
raise ValueError(f"User @{username} not found")
|
|
182
|
+
return _success(_format_user_data(users[0]), "user", start_time)
|
|
183
|
+
|
|
184
|
+
case 'by_id':
|
|
185
|
+
user_id = parameters.get('user_id')
|
|
186
|
+
if not user_id:
|
|
187
|
+
raise ValueError("User ID is required")
|
|
188
|
+
# XDK API: client.users.get_by_ids works similar
|
|
189
|
+
result = client.users.get_by_ids(
|
|
190
|
+
ids=[user_id],
|
|
191
|
+
user_fields=["description", "created_at"]
|
|
192
|
+
)
|
|
193
|
+
users = getattr(result, 'data', []) or []
|
|
194
|
+
if not users:
|
|
195
|
+
raise ValueError(f"User ID {user_id} not found")
|
|
196
|
+
return _success(_format_user_data(users[0]), "user", start_time)
|
|
197
|
+
|
|
198
|
+
case 'followers':
|
|
199
|
+
user_id = parameters.get('user_id')
|
|
200
|
+
if not user_id:
|
|
201
|
+
user_id = await _get_my_user_id(client)
|
|
202
|
+
max_results = min(parameters.get('max_results', 100), 1000)
|
|
203
|
+
users = []
|
|
204
|
+
# XDK API: client.users.get_followers(user_id, max_results=..., user_fields=[...])
|
|
205
|
+
for page in client.users.get_followers(
|
|
206
|
+
user_id,
|
|
207
|
+
max_results=max_results,
|
|
208
|
+
user_fields=["created_at"]
|
|
209
|
+
):
|
|
210
|
+
page_data = getattr(page, 'data', []) or []
|
|
211
|
+
users.extend([_format_user_data(u) for u in page_data])
|
|
212
|
+
break # Only first page
|
|
213
|
+
return _success({"users": users, "count": len(users)}, "followers", start_time)
|
|
214
|
+
|
|
215
|
+
case 'following':
|
|
216
|
+
user_id = parameters.get('user_id')
|
|
217
|
+
if not user_id:
|
|
218
|
+
user_id = await _get_my_user_id(client)
|
|
219
|
+
max_results = min(parameters.get('max_results', 100), 1000)
|
|
220
|
+
users = []
|
|
221
|
+
# XDK API: client.users.get_following(user_id, max_results=..., user_fields=[...])
|
|
222
|
+
for page in client.users.get_following(
|
|
223
|
+
user_id,
|
|
224
|
+
max_results=max_results,
|
|
225
|
+
user_fields=["created_at"]
|
|
226
|
+
):
|
|
227
|
+
page_data = getattr(page, 'data', []) or []
|
|
228
|
+
users.extend([_format_user_data(u) for u in page_data])
|
|
229
|
+
break # Only first page
|
|
230
|
+
return _success({"users": users, "count": len(users)}, "following", start_time)
|
|
231
|
+
|
|
232
|
+
case _:
|
|
233
|
+
raise ValueError(f"Unknown operation: {operation}")
|
|
234
|
+
|
|
235
|
+
except Exception as e:
|
|
236
|
+
logger.error(f"Twitter user error: {e}")
|
|
237
|
+
return {"success": False, "error": str(e), "execution_time": time.time() - start_time}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _success(data: Any, action: str, start_time: float) -> Dict[str, Any]:
|
|
241
|
+
"""Build success response."""
|
|
242
|
+
return {
|
|
243
|
+
"success": True,
|
|
244
|
+
"result": data if isinstance(data, dict) else {"data": data, "action": action},
|
|
245
|
+
"execution_time": time.time() - start_time
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _format_response(response) -> Dict[str, Any]:
|
|
250
|
+
"""Format XDK response object to dict."""
|
|
251
|
+
if hasattr(response, 'data'):
|
|
252
|
+
data = response.data
|
|
253
|
+
if isinstance(data, dict):
|
|
254
|
+
return data
|
|
255
|
+
# Convert object attributes to dict
|
|
256
|
+
return {k: v for k, v in data.__dict__.items() if not k.startswith('_')} if hasattr(data, '__dict__') else {"data": str(data)}
|
|
257
|
+
return {"response": str(response)}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _format_tweet(tweet) -> Dict[str, Any]:
|
|
261
|
+
"""Format tweet object/dict to dict."""
|
|
262
|
+
if isinstance(tweet, dict):
|
|
263
|
+
return {
|
|
264
|
+
"id": tweet.get("id"),
|
|
265
|
+
"text": tweet.get("text"),
|
|
266
|
+
"author_id": tweet.get("author_id"),
|
|
267
|
+
"created_at": str(tweet.get("created_at", "")),
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
"id": getattr(tweet, 'id', None),
|
|
271
|
+
"text": getattr(tweet, 'text', None),
|
|
272
|
+
"author_id": getattr(tweet, 'author_id', None),
|
|
273
|
+
"created_at": str(getattr(tweet, 'created_at', '')),
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _format_user_data(user) -> Dict[str, Any]:
|
|
278
|
+
"""Format user object/dict to dict."""
|
|
279
|
+
if isinstance(user, dict):
|
|
280
|
+
return {
|
|
281
|
+
"id": user.get("id"),
|
|
282
|
+
"username": user.get("username"),
|
|
283
|
+
"name": user.get("name"),
|
|
284
|
+
"profile_image_url": user.get("profile_image_url"),
|
|
285
|
+
"verified": user.get("verified", False),
|
|
286
|
+
"description": user.get("description"),
|
|
287
|
+
"created_at": str(user.get("created_at", "")),
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
"id": getattr(user, 'id', None),
|
|
291
|
+
"username": getattr(user, 'username', None),
|
|
292
|
+
"name": getattr(user, 'name', None),
|
|
293
|
+
"profile_image_url": getattr(user, 'profile_image_url', None),
|
|
294
|
+
"verified": getattr(user, 'verified', False),
|
|
295
|
+
"description": getattr(user, 'description', None),
|
|
296
|
+
"created_at": str(getattr(user, 'created_at', '')),
|
|
297
|
+
}
|