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
|
@@ -1045,6 +1045,163 @@ async def handle_claude_oauth_status(data: Dict[str, Any], websocket: WebSocket)
|
|
|
1045
1045
|
return get_claude_credentials()
|
|
1046
1046
|
|
|
1047
1047
|
|
|
1048
|
+
# ============================================================================
|
|
1049
|
+
# Twitter OAuth Handlers
|
|
1050
|
+
# ============================================================================
|
|
1051
|
+
|
|
1052
|
+
@ws_handler()
|
|
1053
|
+
async def handle_twitter_oauth_login(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
1054
|
+
"""
|
|
1055
|
+
Initiate Twitter OAuth 2.0 with PKCE flow.
|
|
1056
|
+
|
|
1057
|
+
Opens browser to Twitter authorization page. After user authorizes,
|
|
1058
|
+
Twitter redirects to /api/twitter/callback which stores tokens.
|
|
1059
|
+
"""
|
|
1060
|
+
import webbrowser
|
|
1061
|
+
from services.twitter_oauth import TwitterOAuth
|
|
1062
|
+
|
|
1063
|
+
auth_service = container.auth_service()
|
|
1064
|
+
|
|
1065
|
+
# Get stored client credentials (configured via Credentials Modal)
|
|
1066
|
+
client_id = await auth_service.get_api_key("twitter_client_id")
|
|
1067
|
+
client_secret = await auth_service.get_api_key("twitter_client_secret")
|
|
1068
|
+
|
|
1069
|
+
if not client_id:
|
|
1070
|
+
return {
|
|
1071
|
+
"success": False,
|
|
1072
|
+
"error": "Twitter Client ID not configured. Add your Twitter API credentials first."
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
# Create OAuth instance and generate authorization URL
|
|
1076
|
+
oauth = TwitterOAuth(
|
|
1077
|
+
client_id=client_id,
|
|
1078
|
+
client_secret=client_secret,
|
|
1079
|
+
redirect_uri="http://localhost:3010/api/twitter/callback",
|
|
1080
|
+
)
|
|
1081
|
+
|
|
1082
|
+
auth_data = oauth.generate_authorization_url()
|
|
1083
|
+
|
|
1084
|
+
# Open browser to authorization URL
|
|
1085
|
+
webbrowser.open(auth_data["url"])
|
|
1086
|
+
|
|
1087
|
+
return {
|
|
1088
|
+
"success": True,
|
|
1089
|
+
"message": "Opening Twitter authorization in browser...",
|
|
1090
|
+
"state": auth_data["state"],
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
@ws_handler()
|
|
1095
|
+
async def handle_twitter_oauth_status(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
1096
|
+
"""
|
|
1097
|
+
Check Twitter OAuth connection status.
|
|
1098
|
+
|
|
1099
|
+
Returns connection status and user info if connected.
|
|
1100
|
+
"""
|
|
1101
|
+
from services.twitter_oauth import TwitterOAuth
|
|
1102
|
+
|
|
1103
|
+
auth_service = container.auth_service()
|
|
1104
|
+
|
|
1105
|
+
# Check for stored access token
|
|
1106
|
+
access_token = await auth_service.get_api_key("twitter_access_token")
|
|
1107
|
+
|
|
1108
|
+
if not access_token:
|
|
1109
|
+
return {
|
|
1110
|
+
"connected": False,
|
|
1111
|
+
"username": None,
|
|
1112
|
+
"user_id": None,
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
# Get client credentials for API calls
|
|
1116
|
+
client_id = await auth_service.get_api_key("twitter_client_id") or ""
|
|
1117
|
+
client_secret = await auth_service.get_api_key("twitter_client_secret")
|
|
1118
|
+
|
|
1119
|
+
oauth = TwitterOAuth(
|
|
1120
|
+
client_id=client_id,
|
|
1121
|
+
client_secret=client_secret,
|
|
1122
|
+
redirect_uri="http://localhost:3010/api/twitter/callback",
|
|
1123
|
+
)
|
|
1124
|
+
|
|
1125
|
+
# Verify token by getting user info
|
|
1126
|
+
user_info = await oauth.get_user_info(access_token)
|
|
1127
|
+
|
|
1128
|
+
if not user_info.get("success"):
|
|
1129
|
+
# Try to refresh token
|
|
1130
|
+
refresh_token = await auth_service.get_api_key("twitter_refresh_token")
|
|
1131
|
+
if refresh_token:
|
|
1132
|
+
refresh_result = await oauth.refresh_access_token(refresh_token)
|
|
1133
|
+
if refresh_result.get("success"):
|
|
1134
|
+
# Store new tokens
|
|
1135
|
+
await auth_service.store_api_key(
|
|
1136
|
+
provider="twitter_access_token",
|
|
1137
|
+
api_key=refresh_result["access_token"],
|
|
1138
|
+
models=[],
|
|
1139
|
+
session_id="default"
|
|
1140
|
+
)
|
|
1141
|
+
if refresh_result.get("refresh_token"):
|
|
1142
|
+
await auth_service.store_api_key(
|
|
1143
|
+
provider="twitter_refresh_token",
|
|
1144
|
+
api_key=refresh_result["refresh_token"],
|
|
1145
|
+
models=[],
|
|
1146
|
+
session_id="default"
|
|
1147
|
+
)
|
|
1148
|
+
# Retry user info
|
|
1149
|
+
user_info = await oauth.get_user_info(refresh_result["access_token"])
|
|
1150
|
+
|
|
1151
|
+
if not user_info.get("success"):
|
|
1152
|
+
return {
|
|
1153
|
+
"connected": False,
|
|
1154
|
+
"username": None,
|
|
1155
|
+
"user_id": None,
|
|
1156
|
+
"error": user_info.get("error"),
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
return {
|
|
1160
|
+
"connected": True,
|
|
1161
|
+
"username": user_info.get("username"),
|
|
1162
|
+
"user_id": user_info.get("id"),
|
|
1163
|
+
"name": user_info.get("name"),
|
|
1164
|
+
"profile_image_url": user_info.get("profile_image_url"),
|
|
1165
|
+
"verified": user_info.get("verified"),
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
@ws_handler()
|
|
1170
|
+
async def handle_twitter_logout(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
1171
|
+
"""
|
|
1172
|
+
Disconnect Twitter by revoking tokens and clearing stored credentials.
|
|
1173
|
+
"""
|
|
1174
|
+
from services.twitter_oauth import TwitterOAuth
|
|
1175
|
+
|
|
1176
|
+
auth_service = container.auth_service()
|
|
1177
|
+
|
|
1178
|
+
# Get stored tokens
|
|
1179
|
+
access_token = await auth_service.get_api_key("twitter_access_token")
|
|
1180
|
+
refresh_token = await auth_service.get_api_key("twitter_refresh_token")
|
|
1181
|
+
client_id = await auth_service.get_api_key("twitter_client_id") or ""
|
|
1182
|
+
client_secret = await auth_service.get_api_key("twitter_client_secret")
|
|
1183
|
+
|
|
1184
|
+
# Revoke tokens if we have them
|
|
1185
|
+
if access_token or refresh_token:
|
|
1186
|
+
oauth = TwitterOAuth(
|
|
1187
|
+
client_id=client_id,
|
|
1188
|
+
client_secret=client_secret,
|
|
1189
|
+
redirect_uri="http://localhost:3010/api/twitter/callback",
|
|
1190
|
+
)
|
|
1191
|
+
|
|
1192
|
+
if access_token:
|
|
1193
|
+
await oauth.revoke_token(access_token, "access_token")
|
|
1194
|
+
if refresh_token:
|
|
1195
|
+
await oauth.revoke_token(refresh_token, "refresh_token")
|
|
1196
|
+
|
|
1197
|
+
# Clear stored credentials
|
|
1198
|
+
await auth_service.remove_api_key("twitter_access_token")
|
|
1199
|
+
await auth_service.remove_api_key("twitter_refresh_token")
|
|
1200
|
+
await auth_service.remove_api_key("twitter_user_info")
|
|
1201
|
+
|
|
1202
|
+
return {"success": True, "message": "Twitter disconnected"}
|
|
1203
|
+
|
|
1204
|
+
|
|
1048
1205
|
@ws_handler("url")
|
|
1049
1206
|
async def handle_test_ai_proxy(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
1050
1207
|
"""Test connectivity to an AI proxy server."""
|
|
@@ -2059,6 +2216,149 @@ async def handle_configure_compaction(data: Dict[str, Any], websocket: WebSocket
|
|
|
2059
2216
|
return {"success": success}
|
|
2060
2217
|
|
|
2061
2218
|
|
|
2219
|
+
@ws_handler()
|
|
2220
|
+
async def handle_get_provider_usage_summary(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2221
|
+
"""Get aggregated token usage and cost by provider for Credentials Modal."""
|
|
2222
|
+
database = container.database()
|
|
2223
|
+
providers = await database.get_provider_usage_summary()
|
|
2224
|
+
return {"success": True, "providers": providers}
|
|
2225
|
+
|
|
2226
|
+
|
|
2227
|
+
# ============================================================================
|
|
2228
|
+
# Agent Team Handlers
|
|
2229
|
+
# ============================================================================
|
|
2230
|
+
|
|
2231
|
+
@ws_handler("workflow_id", "team_lead_node_id")
|
|
2232
|
+
async def handle_create_team(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2233
|
+
"""Create a new agent team."""
|
|
2234
|
+
from services.agent_team import get_agent_team_service
|
|
2235
|
+
service = get_agent_team_service()
|
|
2236
|
+
team = await service.create_team(
|
|
2237
|
+
team_lead_node_id=data["team_lead_node_id"],
|
|
2238
|
+
teammate_node_ids=data.get("teammates", []),
|
|
2239
|
+
workflow_id=data["workflow_id"],
|
|
2240
|
+
config=data.get("config")
|
|
2241
|
+
)
|
|
2242
|
+
return {"team": team} if team else {"success": False, "error": "Failed to create team"}
|
|
2243
|
+
|
|
2244
|
+
|
|
2245
|
+
@ws_handler("team_id")
|
|
2246
|
+
async def handle_get_team(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2247
|
+
"""Get team info."""
|
|
2248
|
+
database = container.database()
|
|
2249
|
+
team = await database.get_team(data["team_id"])
|
|
2250
|
+
return {"team": team} if team else {"success": False, "error": "Team not found"}
|
|
2251
|
+
|
|
2252
|
+
|
|
2253
|
+
@ws_handler()
|
|
2254
|
+
async def handle_get_team_status(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2255
|
+
"""Get team status with stats.
|
|
2256
|
+
|
|
2257
|
+
Can provide team_id directly, or team_lead_node_id to find active team.
|
|
2258
|
+
"""
|
|
2259
|
+
from services.agent_team import get_agent_team_service
|
|
2260
|
+
|
|
2261
|
+
try:
|
|
2262
|
+
service = get_agent_team_service()
|
|
2263
|
+
except RuntimeError:
|
|
2264
|
+
# Service not initialized - no active teams
|
|
2265
|
+
return {"status": {"members": [], "task_count": 0, "completed_count": 0, "active_count": 0, "pending_count": 0, "failed_count": 0, "active_tasks": []}}
|
|
2266
|
+
|
|
2267
|
+
team_id = data.get("team_id")
|
|
2268
|
+
|
|
2269
|
+
# If no team_id, try to find by team_lead_node_id
|
|
2270
|
+
if not team_id and data.get("team_lead_node_id"):
|
|
2271
|
+
# Look up active team for this workflow (most recent)
|
|
2272
|
+
# This is a simple approach - check if there's an active team with this lead
|
|
2273
|
+
# For now, return empty status - teams are created when AI Employee runs
|
|
2274
|
+
return {"status": {"members": [], "task_count": 0, "completed_count": 0, "active_count": 0, "pending_count": 0, "failed_count": 0, "active_tasks": [], "message": "No active team yet"}}
|
|
2275
|
+
|
|
2276
|
+
if not team_id:
|
|
2277
|
+
return {"status": {"members": [], "task_count": 0, "completed_count": 0, "active_count": 0, "pending_count": 0, "failed_count": 0, "active_tasks": [], "message": "No team connected"}}
|
|
2278
|
+
|
|
2279
|
+
status = await service.get_team_status(team_id)
|
|
2280
|
+
return {"status": status}
|
|
2281
|
+
|
|
2282
|
+
|
|
2283
|
+
@ws_handler("team_id")
|
|
2284
|
+
async def handle_dissolve_team(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2285
|
+
"""Dissolve a team."""
|
|
2286
|
+
from services.agent_team import get_agent_team_service
|
|
2287
|
+
service = get_agent_team_service()
|
|
2288
|
+
success = await service.dissolve_team(data["team_id"], data.get("workflow_id"))
|
|
2289
|
+
return {"success": success}
|
|
2290
|
+
|
|
2291
|
+
|
|
2292
|
+
@ws_handler("team_id", "title", "created_by")
|
|
2293
|
+
async def handle_add_team_task(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2294
|
+
"""Add task to team."""
|
|
2295
|
+
from services.agent_team import get_agent_team_service
|
|
2296
|
+
service = get_agent_team_service()
|
|
2297
|
+
task = await service.add_task(
|
|
2298
|
+
team_id=data["team_id"],
|
|
2299
|
+
title=data["title"],
|
|
2300
|
+
created_by=data["created_by"],
|
|
2301
|
+
description=data.get("description"),
|
|
2302
|
+
priority=data.get("priority", 3),
|
|
2303
|
+
depends_on=data.get("depends_on")
|
|
2304
|
+
)
|
|
2305
|
+
return {"task": task} if task else {"success": False, "error": "Failed to add task"}
|
|
2306
|
+
|
|
2307
|
+
|
|
2308
|
+
@ws_handler("team_id", "task_id", "agent_node_id")
|
|
2309
|
+
async def handle_claim_team_task(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2310
|
+
"""Claim a task."""
|
|
2311
|
+
from services.agent_team import get_agent_team_service
|
|
2312
|
+
service = get_agent_team_service()
|
|
2313
|
+
task = await service.claim_task(data["team_id"], data["task_id"], data["agent_node_id"])
|
|
2314
|
+
return {"task": task} if task else {"success": False, "error": "Task unavailable"}
|
|
2315
|
+
|
|
2316
|
+
|
|
2317
|
+
@ws_handler("team_id", "task_id")
|
|
2318
|
+
async def handle_complete_team_task(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2319
|
+
"""Complete a task."""
|
|
2320
|
+
from services.agent_team import get_agent_team_service
|
|
2321
|
+
service = get_agent_team_service()
|
|
2322
|
+
success = await service.complete_task(data["team_id"], data["task_id"], data.get("result"))
|
|
2323
|
+
return {"success": success}
|
|
2324
|
+
|
|
2325
|
+
|
|
2326
|
+
@ws_handler("team_id")
|
|
2327
|
+
async def handle_get_team_tasks(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2328
|
+
"""Get team tasks."""
|
|
2329
|
+
database = container.database()
|
|
2330
|
+
tasks = await database.get_team_tasks(data["team_id"], data.get("status"))
|
|
2331
|
+
return {"tasks": tasks}
|
|
2332
|
+
|
|
2333
|
+
|
|
2334
|
+
@ws_handler("team_id", "from_agent", "content")
|
|
2335
|
+
async def handle_send_team_message(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2336
|
+
"""Send message in team."""
|
|
2337
|
+
from services.agent_team import get_agent_team_service
|
|
2338
|
+
service = get_agent_team_service()
|
|
2339
|
+
msg = await service.send_message(
|
|
2340
|
+
team_id=data["team_id"],
|
|
2341
|
+
from_agent=data["from_agent"],
|
|
2342
|
+
content=data["content"],
|
|
2343
|
+
to_agent=data.get("to_agent"),
|
|
2344
|
+
message_type=data.get("message_type", "direct")
|
|
2345
|
+
)
|
|
2346
|
+
return {"message": msg} if msg else {"success": False, "error": "Failed to send message"}
|
|
2347
|
+
|
|
2348
|
+
|
|
2349
|
+
@ws_handler("team_id")
|
|
2350
|
+
async def handle_get_team_messages(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2351
|
+
"""Get team messages."""
|
|
2352
|
+
from services.agent_team import get_agent_team_service
|
|
2353
|
+
service = get_agent_team_service()
|
|
2354
|
+
messages = await service.get_messages(
|
|
2355
|
+
team_id=data["team_id"],
|
|
2356
|
+
agent_node_id=data.get("agent_node_id"),
|
|
2357
|
+
unread_only=data.get("unread_only", False)
|
|
2358
|
+
)
|
|
2359
|
+
return {"messages": messages}
|
|
2360
|
+
|
|
2361
|
+
|
|
2062
2362
|
# ============================================================================
|
|
2063
2363
|
# Message Router
|
|
2064
2364
|
# ============================================================================
|
|
@@ -2123,6 +2423,11 @@ MESSAGE_HANDLERS: Dict[str, MessageHandler] = {
|
|
|
2123
2423
|
"claude_oauth_login": handle_claude_oauth_login,
|
|
2124
2424
|
"claude_oauth_status": handle_claude_oauth_status,
|
|
2125
2425
|
|
|
2426
|
+
# Twitter OAuth operations
|
|
2427
|
+
"twitter_oauth_login": handle_twitter_oauth_login,
|
|
2428
|
+
"twitter_oauth_status": handle_twitter_oauth_status,
|
|
2429
|
+
"twitter_logout": handle_twitter_logout,
|
|
2430
|
+
|
|
2126
2431
|
# Android operations
|
|
2127
2432
|
"get_android_devices": handle_get_android_devices,
|
|
2128
2433
|
"execute_android_action": handle_execute_android_action,
|
|
@@ -2195,6 +2500,21 @@ MESSAGE_HANDLERS: Dict[str, MessageHandler] = {
|
|
|
2195
2500
|
# Compaction
|
|
2196
2501
|
"get_compaction_stats": handle_get_compaction_stats,
|
|
2197
2502
|
"configure_compaction": handle_configure_compaction,
|
|
2503
|
+
|
|
2504
|
+
# Provider Usage Summary
|
|
2505
|
+
"get_provider_usage_summary": handle_get_provider_usage_summary,
|
|
2506
|
+
|
|
2507
|
+
# Agent Teams
|
|
2508
|
+
"create_team": handle_create_team,
|
|
2509
|
+
"get_team": handle_get_team,
|
|
2510
|
+
"get_team_status": handle_get_team_status,
|
|
2511
|
+
"dissolve_team": handle_dissolve_team,
|
|
2512
|
+
"add_team_task": handle_add_team_task,
|
|
2513
|
+
"claim_team_task": handle_claim_team_task,
|
|
2514
|
+
"complete_team_task": handle_complete_team_task,
|
|
2515
|
+
"get_team_tasks": handle_get_team_tasks,
|
|
2516
|
+
"send_team_message": handle_send_team_message,
|
|
2517
|
+
"get_team_messages": handle_get_team_messages,
|
|
2198
2518
|
}
|
|
2199
2519
|
|
|
2200
2520
|
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Agent Team Service - Claude SDK Agent Teams pattern.
|
|
2
|
+
|
|
3
|
+
Coordinates multi-agent teams with shared task lists and messaging.
|
|
4
|
+
Teams are scoped to specific workflow executions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import uuid
|
|
8
|
+
from typing import Dict, Any, List, Optional
|
|
9
|
+
from core.database import Database
|
|
10
|
+
from core.logging import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AgentTeamService:
|
|
16
|
+
"""Service for managing agent teams.
|
|
17
|
+
|
|
18
|
+
Teams are workflow-specific - each team belongs to a workflow execution.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, database: Database, broadcaster=None):
|
|
22
|
+
self.database = database
|
|
23
|
+
self.broadcaster = broadcaster
|
|
24
|
+
# Track active teams per workflow
|
|
25
|
+
self._active_teams: Dict[str, str] = {} # workflow_id -> team_id
|
|
26
|
+
|
|
27
|
+
# -------------------------------------------------------------------------
|
|
28
|
+
# Team Lifecycle
|
|
29
|
+
# -------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
async def create_team(
|
|
32
|
+
self,
|
|
33
|
+
team_lead_node_id: str,
|
|
34
|
+
teammate_node_ids: List[Dict[str, Any]],
|
|
35
|
+
workflow_id: str,
|
|
36
|
+
config: Optional[Dict[str, Any]] = None
|
|
37
|
+
) -> Optional[Dict[str, Any]]:
|
|
38
|
+
"""Create a team with lead and teammates.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
team_lead_node_id: Node ID of the team lead agent
|
|
42
|
+
teammate_node_ids: List of {node_id, node_type, label} for teammates
|
|
43
|
+
workflow_id: Workflow containing the team
|
|
44
|
+
config: Team configuration (mode: parallel/sequential/hybrid)
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Team dict or None on failure
|
|
48
|
+
"""
|
|
49
|
+
team_id = f"team_{uuid.uuid4().hex[:12]}"
|
|
50
|
+
|
|
51
|
+
# Create team
|
|
52
|
+
team = await self.database.create_team(
|
|
53
|
+
team_id=team_id,
|
|
54
|
+
workflow_id=workflow_id,
|
|
55
|
+
team_lead_node_id=team_lead_node_id,
|
|
56
|
+
config=config or {"mode": "parallel"}
|
|
57
|
+
)
|
|
58
|
+
if not team:
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
# Add team lead as member
|
|
62
|
+
await self.database.add_team_member(
|
|
63
|
+
team_id=team_id,
|
|
64
|
+
agent_node_id=team_lead_node_id,
|
|
65
|
+
agent_type="orchestrator_agent",
|
|
66
|
+
role="team_lead"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Add teammates
|
|
70
|
+
for teammate in teammate_node_ids:
|
|
71
|
+
await self.database.add_team_member(
|
|
72
|
+
team_id=team_id,
|
|
73
|
+
agent_node_id=teammate["node_id"],
|
|
74
|
+
agent_type=teammate.get("node_type", "agent"),
|
|
75
|
+
agent_label=teammate.get("label"),
|
|
76
|
+
role="teammate"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Broadcast team creation
|
|
80
|
+
if self.broadcaster:
|
|
81
|
+
await self.broadcaster.broadcast_team_event(team_id, "team_created", {
|
|
82
|
+
"team_id": team_id,
|
|
83
|
+
"workflow_id": workflow_id,
|
|
84
|
+
"member_count": len(teammate_node_ids) + 1
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
# Track active team for this workflow
|
|
88
|
+
self._active_teams[workflow_id] = team_id
|
|
89
|
+
|
|
90
|
+
logger.info(f"[Teams] Created team {team_id} with {len(teammate_node_ids)} teammates")
|
|
91
|
+
return {"team_id": team_id, **team}
|
|
92
|
+
|
|
93
|
+
def get_active_team_for_workflow(self, workflow_id: str) -> Optional[str]:
|
|
94
|
+
"""Get the active team ID for a workflow."""
|
|
95
|
+
return self._active_teams.get(workflow_id)
|
|
96
|
+
|
|
97
|
+
async def dissolve_team(self, team_id: str, workflow_id: Optional[str] = None) -> bool:
|
|
98
|
+
"""Dissolve a team."""
|
|
99
|
+
success = await self.database.update_team_status(team_id, "dissolved")
|
|
100
|
+
if success:
|
|
101
|
+
# Remove from active teams tracking
|
|
102
|
+
if workflow_id and workflow_id in self._active_teams:
|
|
103
|
+
del self._active_teams[workflow_id]
|
|
104
|
+
if self.broadcaster:
|
|
105
|
+
await self.broadcaster.broadcast_team_event(team_id, "team_dissolved", {"team_id": team_id})
|
|
106
|
+
return success
|
|
107
|
+
|
|
108
|
+
async def get_team_status(self, team_id: str) -> Dict[str, Any]:
|
|
109
|
+
"""Get comprehensive team status."""
|
|
110
|
+
return await self.database.get_team_stats(team_id)
|
|
111
|
+
|
|
112
|
+
# -------------------------------------------------------------------------
|
|
113
|
+
# Task Management
|
|
114
|
+
# -------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
async def add_task(
|
|
117
|
+
self,
|
|
118
|
+
team_id: str,
|
|
119
|
+
title: str,
|
|
120
|
+
created_by: str,
|
|
121
|
+
description: Optional[str] = None,
|
|
122
|
+
priority: int = 3,
|
|
123
|
+
depends_on: Optional[List[str]] = None
|
|
124
|
+
) -> Optional[Dict[str, Any]]:
|
|
125
|
+
"""Add a task to the shared task list."""
|
|
126
|
+
task_id = f"task_{uuid.uuid4().hex[:12]}"
|
|
127
|
+
|
|
128
|
+
task = await self.database.add_team_task(
|
|
129
|
+
task_id=task_id,
|
|
130
|
+
team_id=team_id,
|
|
131
|
+
title=title,
|
|
132
|
+
created_by=created_by,
|
|
133
|
+
description=description,
|
|
134
|
+
priority=priority,
|
|
135
|
+
depends_on=depends_on
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if task and self.broadcaster:
|
|
139
|
+
await self.broadcaster.broadcast_team_event(team_id, "task_added", task)
|
|
140
|
+
|
|
141
|
+
return task
|
|
142
|
+
|
|
143
|
+
async def claim_task(self, team_id: str, task_id: str, agent_node_id: str) -> Optional[Dict[str, Any]]:
|
|
144
|
+
"""Claim a task for an agent."""
|
|
145
|
+
# Update member status to working
|
|
146
|
+
await self.database.update_member_status(team_id, agent_node_id, "working")
|
|
147
|
+
|
|
148
|
+
task = await self.database.claim_task(task_id, agent_node_id)
|
|
149
|
+
|
|
150
|
+
if task and self.broadcaster:
|
|
151
|
+
await self.broadcaster.broadcast_team_event(team_id, "task_claimed", {
|
|
152
|
+
**task, "claimed_by": agent_node_id
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
return task
|
|
156
|
+
|
|
157
|
+
async def complete_task(self, team_id: str, task_id: str, result: Optional[Dict[str, Any]] = None) -> bool:
|
|
158
|
+
"""Mark a task as completed."""
|
|
159
|
+
# Get task to find assigned agent
|
|
160
|
+
tasks = await self.database.get_team_tasks(team_id)
|
|
161
|
+
task = next((t for t in tasks if t["id"] == task_id), None)
|
|
162
|
+
|
|
163
|
+
success = await self.database.complete_task(task_id, result)
|
|
164
|
+
|
|
165
|
+
if success:
|
|
166
|
+
# Update member status back to idle
|
|
167
|
+
if task and task.get("assigned_to"):
|
|
168
|
+
await self.database.update_member_status(team_id, task["assigned_to"], "idle")
|
|
169
|
+
|
|
170
|
+
if self.broadcaster:
|
|
171
|
+
await self.broadcaster.broadcast_team_event(team_id, "task_completed", {
|
|
172
|
+
"task_id": task_id, "result": result
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
return success
|
|
176
|
+
|
|
177
|
+
async def fail_task(self, team_id: str, task_id: str, error: str) -> bool:
|
|
178
|
+
"""Mark a task as failed."""
|
|
179
|
+
tasks = await self.database.get_team_tasks(team_id)
|
|
180
|
+
task = next((t for t in tasks if t["id"] == task_id), None)
|
|
181
|
+
|
|
182
|
+
success = await self.database.fail_task(task_id, error)
|
|
183
|
+
|
|
184
|
+
if success:
|
|
185
|
+
if task and task.get("assigned_to"):
|
|
186
|
+
await self.database.update_member_status(team_id, task["assigned_to"], "idle")
|
|
187
|
+
|
|
188
|
+
if self.broadcaster:
|
|
189
|
+
await self.broadcaster.broadcast_team_event(team_id, "task_failed", {
|
|
190
|
+
"task_id": task_id, "error": error
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
return success
|
|
194
|
+
|
|
195
|
+
async def get_claimable_tasks(self, team_id: str) -> List[Dict[str, Any]]:
|
|
196
|
+
"""Get tasks ready to be claimed."""
|
|
197
|
+
return await self.database.get_claimable_tasks(team_id)
|
|
198
|
+
|
|
199
|
+
async def is_team_done(self, team_id: str) -> bool:
|
|
200
|
+
"""Check if all tasks are completed/failed."""
|
|
201
|
+
tasks = await self.database.get_team_tasks(team_id)
|
|
202
|
+
if not tasks:
|
|
203
|
+
return True
|
|
204
|
+
return all(t["status"] in ("completed", "failed", "skipped") for t in tasks)
|
|
205
|
+
|
|
206
|
+
# -------------------------------------------------------------------------
|
|
207
|
+
# Messaging
|
|
208
|
+
# -------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
async def send_message(
|
|
211
|
+
self,
|
|
212
|
+
team_id: str,
|
|
213
|
+
from_agent: str,
|
|
214
|
+
content: str,
|
|
215
|
+
to_agent: Optional[str] = None,
|
|
216
|
+
message_type: str = "direct"
|
|
217
|
+
) -> Optional[Dict[str, Any]]:
|
|
218
|
+
"""Send a message to a specific agent or broadcast."""
|
|
219
|
+
msg = await self.database.add_agent_message(
|
|
220
|
+
team_id=team_id,
|
|
221
|
+
from_agent=from_agent,
|
|
222
|
+
content=content,
|
|
223
|
+
message_type=message_type if to_agent else "broadcast",
|
|
224
|
+
to_agent=to_agent
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
if msg and self.broadcaster:
|
|
228
|
+
await self.broadcaster.broadcast_team_event(team_id, "team_message", msg)
|
|
229
|
+
|
|
230
|
+
return msg
|
|
231
|
+
|
|
232
|
+
async def broadcast(self, team_id: str, from_agent: str, content: str) -> Optional[Dict[str, Any]]:
|
|
233
|
+
"""Broadcast message to all team members."""
|
|
234
|
+
return await self.send_message(team_id, from_agent, content, to_agent=None)
|
|
235
|
+
|
|
236
|
+
async def get_messages(
|
|
237
|
+
self,
|
|
238
|
+
team_id: str,
|
|
239
|
+
agent_node_id: Optional[str] = None,
|
|
240
|
+
unread_only: bool = False
|
|
241
|
+
) -> List[Dict[str, Any]]:
|
|
242
|
+
"""Get messages for a team or specific agent."""
|
|
243
|
+
return await self.database.get_agent_messages(team_id, agent_node_id, unread_only)
|
|
244
|
+
|
|
245
|
+
async def mark_read(self, team_id: str, agent_node_id: str) -> int:
|
|
246
|
+
"""Mark all messages as read for an agent."""
|
|
247
|
+
return await self.database.mark_messages_read(team_id, agent_node_id)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# Singleton instance
|
|
251
|
+
_service: Optional[AgentTeamService] = None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def get_agent_team_service() -> AgentTeamService:
|
|
255
|
+
"""Get the singleton AgentTeamService instance."""
|
|
256
|
+
global _service
|
|
257
|
+
if _service is None:
|
|
258
|
+
raise RuntimeError("AgentTeamService not initialized. Call init_agent_team_service first.")
|
|
259
|
+
return _service
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def init_agent_team_service(database: Database, broadcaster=None) -> AgentTeamService:
|
|
263
|
+
"""Initialize the singleton AgentTeamService."""
|
|
264
|
+
global _service
|
|
265
|
+
_service = AgentTeamService(database, broadcaster)
|
|
266
|
+
return _service
|
package/server/services/ai.py
CHANGED
|
@@ -2917,6 +2917,49 @@ class AIService:
|
|
|
2917
2917
|
|
|
2918
2918
|
return TaskManagerSchema
|
|
2919
2919
|
|
|
2920
|
+
# Twitter Send tool schema (dual-purpose: workflow node + AI tool)
|
|
2921
|
+
if node_type == 'twitterSend':
|
|
2922
|
+
class TwitterSendSchema(BaseModel):
|
|
2923
|
+
"""Post tweets, replies, retweets, and likes on Twitter/X."""
|
|
2924
|
+
action: str = Field(
|
|
2925
|
+
default="tweet",
|
|
2926
|
+
description="Action: tweet, reply, retweet, like, unlike, delete"
|
|
2927
|
+
)
|
|
2928
|
+
text: Optional[str] = Field(
|
|
2929
|
+
default=None,
|
|
2930
|
+
description="Tweet text (max 280 chars). Required for tweet/reply."
|
|
2931
|
+
)
|
|
2932
|
+
tweet_id: Optional[str] = Field(
|
|
2933
|
+
default=None,
|
|
2934
|
+
description="Tweet ID for reply/retweet/like/unlike/delete actions."
|
|
2935
|
+
)
|
|
2936
|
+
reply_to_id: Optional[str] = Field(
|
|
2937
|
+
default=None,
|
|
2938
|
+
description="Tweet ID to reply to (alias for tweet_id when action=reply)."
|
|
2939
|
+
)
|
|
2940
|
+
|
|
2941
|
+
return TwitterSendSchema
|
|
2942
|
+
|
|
2943
|
+
# Twitter Search tool schema (dual-purpose: workflow node + AI tool)
|
|
2944
|
+
if node_type == 'twitterSearch':
|
|
2945
|
+
class TwitterSearchSchema(BaseModel):
|
|
2946
|
+
"""Search recent tweets on Twitter/X."""
|
|
2947
|
+
query: str = Field(description="Search query (supports X search operators)")
|
|
2948
|
+
max_results: int = Field(default=10, description="Max results (10-100)")
|
|
2949
|
+
|
|
2950
|
+
return TwitterSearchSchema
|
|
2951
|
+
|
|
2952
|
+
# Twitter User tool schema (dual-purpose: workflow node + AI tool)
|
|
2953
|
+
if node_type == 'twitterUser':
|
|
2954
|
+
class TwitterUserSchema(BaseModel):
|
|
2955
|
+
"""Look up Twitter/X user profiles and social connections."""
|
|
2956
|
+
operation: str = Field(default="me", description="Operation: me, by_username, by_id, followers, following")
|
|
2957
|
+
username: Optional[str] = Field(default=None, description="Twitter username (without @) for by_username")
|
|
2958
|
+
user_id: Optional[str] = Field(default=None, description="Twitter user ID for by_id/followers/following")
|
|
2959
|
+
max_results: int = Field(default=100, description="Max results for followers/following (1-1000)")
|
|
2960
|
+
|
|
2961
|
+
return TwitterUserSchema
|
|
2962
|
+
|
|
2920
2963
|
# Check delegated tasks schema (built-in tool for result retrieval)
|
|
2921
2964
|
if node_type == '_builtin_check_delegated_tasks':
|
|
2922
2965
|
class CheckDelegatedTasksSchema(BaseModel):
|