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
package/server/core/database.py
CHANGED
|
@@ -12,7 +12,8 @@ from core.config import Settings
|
|
|
12
12
|
from models.database import (
|
|
13
13
|
NodeParameter, Workflow, Execution, APIKey, APIKeyValidation, NodeOutput,
|
|
14
14
|
ConversationMessage, ToolSchema, UserSkill, ChatMessage, UserSettings,
|
|
15
|
-
TokenUsageMetric, CompactionEvent, SessionTokenState, ProviderDefaults
|
|
15
|
+
TokenUsageMetric, CompactionEvent, SessionTokenState, ProviderDefaults,
|
|
16
|
+
AgentTeam, TeamMember, TeamTask, AgentMessage
|
|
16
17
|
)
|
|
17
18
|
from models.cache import CacheEntry # SQLite-backed cache for Redis alternative
|
|
18
19
|
from models.auth import User # Import User model to ensure table creation
|
|
@@ -87,6 +88,29 @@ class Database:
|
|
|
87
88
|
"ALTER TABLE user_settings ADD COLUMN examples_loaded BOOLEAN DEFAULT 0"
|
|
88
89
|
))
|
|
89
90
|
logger.info("Added examples_loaded column to user_settings")
|
|
91
|
+
|
|
92
|
+
# Migrate token_usage_metrics table - add cost columns
|
|
93
|
+
result = await conn.execute(text("PRAGMA table_info(token_usage_metrics)"))
|
|
94
|
+
columns = {row[1] for row in result.fetchall()}
|
|
95
|
+
|
|
96
|
+
for col in ["input_cost", "output_cost", "cache_cost", "total_cost"]:
|
|
97
|
+
if col not in columns:
|
|
98
|
+
await conn.execute(text(
|
|
99
|
+
f"ALTER TABLE token_usage_metrics ADD COLUMN {col} REAL DEFAULT 0.0"
|
|
100
|
+
))
|
|
101
|
+
logger.info(f"Added {col} column to token_usage_metrics")
|
|
102
|
+
|
|
103
|
+
# Migrate session_token_states table - add cumulative cost columns
|
|
104
|
+
result = await conn.execute(text("PRAGMA table_info(session_token_states)"))
|
|
105
|
+
columns = {row[1] for row in result.fetchall()}
|
|
106
|
+
|
|
107
|
+
for col in ["cumulative_input_cost", "cumulative_output_cost", "cumulative_total_cost"]:
|
|
108
|
+
if col not in columns:
|
|
109
|
+
await conn.execute(text(
|
|
110
|
+
f"ALTER TABLE session_token_states ADD COLUMN {col} REAL DEFAULT 0.0"
|
|
111
|
+
))
|
|
112
|
+
logger.info(f"Added {col} column to session_token_states")
|
|
113
|
+
|
|
90
114
|
except Exception as e:
|
|
91
115
|
logger.warning(f"Migration check failed (table may not exist yet): {e}")
|
|
92
116
|
|
|
@@ -1568,7 +1592,12 @@ class Database:
|
|
|
1568
1592
|
reasoning_tokens=metric.get("reasoning_tokens", 0),
|
|
1569
1593
|
iteration=metric.get("iteration", 1),
|
|
1570
1594
|
execution_id=metric.get("execution_id"),
|
|
1571
|
-
created_at=datetime.now(timezone.utc)
|
|
1595
|
+
created_at=datetime.now(timezone.utc),
|
|
1596
|
+
# Cost fields
|
|
1597
|
+
input_cost=metric.get("input_cost", 0.0),
|
|
1598
|
+
output_cost=metric.get("output_cost", 0.0),
|
|
1599
|
+
cache_cost=metric.get("cache_cost", 0.0),
|
|
1600
|
+
total_cost=metric.get("total_cost", 0.0)
|
|
1572
1601
|
)
|
|
1573
1602
|
session.add(entry)
|
|
1574
1603
|
await session.commit()
|
|
@@ -1614,6 +1643,97 @@ class Database:
|
|
|
1614
1643
|
logger.error("Failed to get token metrics", session_id=session_id, error=str(e))
|
|
1615
1644
|
return []
|
|
1616
1645
|
|
|
1646
|
+
async def get_provider_usage_summary(self) -> List[Dict[str, Any]]:
|
|
1647
|
+
"""Get aggregated token usage and cost by provider.
|
|
1648
|
+
|
|
1649
|
+
Returns a list of provider summaries with:
|
|
1650
|
+
- provider: Provider name (openai, anthropic, etc.)
|
|
1651
|
+
- total_input_tokens: Sum of input tokens
|
|
1652
|
+
- total_output_tokens: Sum of output tokens
|
|
1653
|
+
- total_tokens: Sum of all tokens
|
|
1654
|
+
- total_input_cost: Sum of input costs (USD)
|
|
1655
|
+
- total_output_cost: Sum of output costs (USD)
|
|
1656
|
+
- total_cost: Sum of all costs (USD)
|
|
1657
|
+
- execution_count: Number of executions
|
|
1658
|
+
- models: Breakdown by model (list of dicts)
|
|
1659
|
+
"""
|
|
1660
|
+
try:
|
|
1661
|
+
async with self.get_session() as session:
|
|
1662
|
+
from sqlalchemy import func
|
|
1663
|
+
|
|
1664
|
+
# First get per-model breakdown
|
|
1665
|
+
model_stmt = (
|
|
1666
|
+
select(
|
|
1667
|
+
TokenUsageMetric.provider,
|
|
1668
|
+
TokenUsageMetric.model,
|
|
1669
|
+
func.sum(TokenUsageMetric.input_tokens).label("input_tokens"),
|
|
1670
|
+
func.sum(TokenUsageMetric.output_tokens).label("output_tokens"),
|
|
1671
|
+
func.sum(TokenUsageMetric.total_tokens).label("total_tokens"),
|
|
1672
|
+
func.sum(TokenUsageMetric.input_cost).label("input_cost"),
|
|
1673
|
+
func.sum(TokenUsageMetric.output_cost).label("output_cost"),
|
|
1674
|
+
func.sum(TokenUsageMetric.cache_cost).label("cache_cost"),
|
|
1675
|
+
func.sum(TokenUsageMetric.total_cost).label("total_cost"),
|
|
1676
|
+
func.count().label("execution_count")
|
|
1677
|
+
)
|
|
1678
|
+
.group_by(TokenUsageMetric.provider, TokenUsageMetric.model)
|
|
1679
|
+
.order_by(TokenUsageMetric.provider, TokenUsageMetric.model)
|
|
1680
|
+
)
|
|
1681
|
+
result = await session.execute(model_stmt)
|
|
1682
|
+
rows = result.all()
|
|
1683
|
+
|
|
1684
|
+
# Aggregate by provider
|
|
1685
|
+
providers: Dict[str, Dict] = {}
|
|
1686
|
+
for row in rows:
|
|
1687
|
+
provider = row.provider or "unknown"
|
|
1688
|
+
if provider not in providers:
|
|
1689
|
+
providers[provider] = {
|
|
1690
|
+
"provider": provider,
|
|
1691
|
+
"total_input_tokens": 0,
|
|
1692
|
+
"total_output_tokens": 0,
|
|
1693
|
+
"total_tokens": 0,
|
|
1694
|
+
"total_input_cost": 0.0,
|
|
1695
|
+
"total_output_cost": 0.0,
|
|
1696
|
+
"total_cache_cost": 0.0,
|
|
1697
|
+
"total_cost": 0.0,
|
|
1698
|
+
"execution_count": 0,
|
|
1699
|
+
"models": []
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
p = providers[provider]
|
|
1703
|
+
p["total_input_tokens"] += row.input_tokens or 0
|
|
1704
|
+
p["total_output_tokens"] += row.output_tokens or 0
|
|
1705
|
+
p["total_tokens"] += row.total_tokens or 0
|
|
1706
|
+
p["total_input_cost"] += float(row.input_cost or 0)
|
|
1707
|
+
p["total_output_cost"] += float(row.output_cost or 0)
|
|
1708
|
+
p["total_cache_cost"] += float(row.cache_cost or 0)
|
|
1709
|
+
p["total_cost"] += float(row.total_cost or 0)
|
|
1710
|
+
p["execution_count"] += row.execution_count or 0
|
|
1711
|
+
|
|
1712
|
+
p["models"].append({
|
|
1713
|
+
"model": row.model,
|
|
1714
|
+
"input_tokens": row.input_tokens or 0,
|
|
1715
|
+
"output_tokens": row.output_tokens or 0,
|
|
1716
|
+
"total_tokens": row.total_tokens or 0,
|
|
1717
|
+
"input_cost": round(float(row.input_cost or 0), 6),
|
|
1718
|
+
"output_cost": round(float(row.output_cost or 0), 6),
|
|
1719
|
+
"cache_cost": round(float(row.cache_cost or 0), 6),
|
|
1720
|
+
"total_cost": round(float(row.total_cost or 0), 6),
|
|
1721
|
+
"execution_count": row.execution_count or 0
|
|
1722
|
+
})
|
|
1723
|
+
|
|
1724
|
+
# Round provider totals
|
|
1725
|
+
for p in providers.values():
|
|
1726
|
+
p["total_input_cost"] = round(p["total_input_cost"], 6)
|
|
1727
|
+
p["total_output_cost"] = round(p["total_output_cost"], 6)
|
|
1728
|
+
p["total_cache_cost"] = round(p["total_cache_cost"], 6)
|
|
1729
|
+
p["total_cost"] = round(p["total_cost"], 6)
|
|
1730
|
+
|
|
1731
|
+
return list(providers.values())
|
|
1732
|
+
|
|
1733
|
+
except Exception as e:
|
|
1734
|
+
logger.error("Failed to get provider usage summary", error=str(e))
|
|
1735
|
+
return []
|
|
1736
|
+
|
|
1617
1737
|
# ============================================================================
|
|
1618
1738
|
# Session Token State
|
|
1619
1739
|
# ============================================================================
|
|
@@ -1786,4 +1906,324 @@ class Database:
|
|
|
1786
1906
|
]
|
|
1787
1907
|
except Exception as e:
|
|
1788
1908
|
logger.error("Failed to get compaction history", session_id=session_id, error=str(e))
|
|
1789
|
-
return []
|
|
1909
|
+
return []
|
|
1910
|
+
|
|
1911
|
+
# ============================================================================
|
|
1912
|
+
# Agent Teams - CRUD Operations
|
|
1913
|
+
# ============================================================================
|
|
1914
|
+
|
|
1915
|
+
async def create_team(self, team_id: str, workflow_id: str, team_lead_node_id: str,
|
|
1916
|
+
config: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
|
|
1917
|
+
"""Create a new agent team."""
|
|
1918
|
+
try:
|
|
1919
|
+
async with self.get_session() as session:
|
|
1920
|
+
team = AgentTeam(
|
|
1921
|
+
id=team_id, workflow_id=workflow_id, team_lead_node_id=team_lead_node_id,
|
|
1922
|
+
config=config or {}, created_at=datetime.now(timezone.utc)
|
|
1923
|
+
)
|
|
1924
|
+
session.add(team)
|
|
1925
|
+
await session.commit()
|
|
1926
|
+
logger.info(f"[Teams] Created team {team_id}")
|
|
1927
|
+
return {"id": team.id, "workflow_id": team.workflow_id, "status": team.status}
|
|
1928
|
+
except Exception as e:
|
|
1929
|
+
logger.error(f"Failed to create team: {e}")
|
|
1930
|
+
return None
|
|
1931
|
+
|
|
1932
|
+
async def get_team(self, team_id: str) -> Optional[Dict[str, Any]]:
|
|
1933
|
+
"""Get team by ID."""
|
|
1934
|
+
try:
|
|
1935
|
+
async with self.get_session() as session:
|
|
1936
|
+
result = await session.execute(select(AgentTeam).where(AgentTeam.id == team_id))
|
|
1937
|
+
team = result.scalar_one_or_none()
|
|
1938
|
+
if not team:
|
|
1939
|
+
return None
|
|
1940
|
+
return {
|
|
1941
|
+
"id": team.id, "workflow_id": team.workflow_id, "team_lead_node_id": team.team_lead_node_id,
|
|
1942
|
+
"status": team.status, "config": team.config,
|
|
1943
|
+
"created_at": team.created_at.isoformat() if team.created_at else None
|
|
1944
|
+
}
|
|
1945
|
+
except Exception as e:
|
|
1946
|
+
logger.error(f"Failed to get team: {e}")
|
|
1947
|
+
return None
|
|
1948
|
+
|
|
1949
|
+
async def update_team_status(self, team_id: str, status: str) -> bool:
|
|
1950
|
+
"""Update team status."""
|
|
1951
|
+
try:
|
|
1952
|
+
async with self.get_session() as session:
|
|
1953
|
+
result = await session.execute(select(AgentTeam).where(AgentTeam.id == team_id))
|
|
1954
|
+
team = result.scalar_one_or_none()
|
|
1955
|
+
if not team:
|
|
1956
|
+
return False
|
|
1957
|
+
team.status = status
|
|
1958
|
+
if status in ("completed", "failed", "dissolved"):
|
|
1959
|
+
team.completed_at = datetime.now(timezone.utc)
|
|
1960
|
+
await session.commit()
|
|
1961
|
+
return True
|
|
1962
|
+
except Exception as e:
|
|
1963
|
+
logger.error(f"Failed to update team status: {e}")
|
|
1964
|
+
return False
|
|
1965
|
+
|
|
1966
|
+
async def add_team_member(self, team_id: str, agent_node_id: str, agent_type: str,
|
|
1967
|
+
role: str = "teammate", agent_label: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
|
1968
|
+
"""Add member to team."""
|
|
1969
|
+
try:
|
|
1970
|
+
async with self.get_session() as session:
|
|
1971
|
+
member = TeamMember(
|
|
1972
|
+
team_id=team_id, agent_node_id=agent_node_id, agent_type=agent_type,
|
|
1973
|
+
agent_label=agent_label, role=role, joined_at=datetime.now(timezone.utc)
|
|
1974
|
+
)
|
|
1975
|
+
session.add(member)
|
|
1976
|
+
await session.commit()
|
|
1977
|
+
return {"id": member.id, "agent_node_id": agent_node_id, "role": role}
|
|
1978
|
+
except Exception as e:
|
|
1979
|
+
logger.error(f"Failed to add team member: {e}")
|
|
1980
|
+
return None
|
|
1981
|
+
|
|
1982
|
+
async def get_team_members(self, team_id: str) -> List[Dict[str, Any]]:
|
|
1983
|
+
"""Get all team members."""
|
|
1984
|
+
try:
|
|
1985
|
+
async with self.get_session() as session:
|
|
1986
|
+
result = await session.execute(
|
|
1987
|
+
select(TeamMember).where(TeamMember.team_id == team_id)
|
|
1988
|
+
)
|
|
1989
|
+
return [
|
|
1990
|
+
{"id": m.id, "agent_node_id": m.agent_node_id, "agent_type": m.agent_type,
|
|
1991
|
+
"agent_label": m.agent_label, "role": m.role, "status": m.status}
|
|
1992
|
+
for m in result.scalars().all()
|
|
1993
|
+
]
|
|
1994
|
+
except Exception as e:
|
|
1995
|
+
logger.error(f"Failed to get team members: {e}")
|
|
1996
|
+
return []
|
|
1997
|
+
|
|
1998
|
+
async def update_member_status(self, team_id: str, agent_node_id: str, status: str) -> bool:
|
|
1999
|
+
"""Update member status (idle, working, offline)."""
|
|
2000
|
+
try:
|
|
2001
|
+
async with self.get_session() as session:
|
|
2002
|
+
result = await session.execute(
|
|
2003
|
+
select(TeamMember).where(
|
|
2004
|
+
TeamMember.team_id == team_id, TeamMember.agent_node_id == agent_node_id
|
|
2005
|
+
)
|
|
2006
|
+
)
|
|
2007
|
+
member = result.scalar_one_or_none()
|
|
2008
|
+
if not member:
|
|
2009
|
+
return False
|
|
2010
|
+
member.status = status
|
|
2011
|
+
await session.commit()
|
|
2012
|
+
return True
|
|
2013
|
+
except Exception as e:
|
|
2014
|
+
logger.error(f"Failed to update member status: {e}")
|
|
2015
|
+
return False
|
|
2016
|
+
|
|
2017
|
+
async def add_team_task(self, task_id: str, team_id: str, title: str, created_by: str,
|
|
2018
|
+
description: Optional[str] = None, priority: int = 3,
|
|
2019
|
+
depends_on: Optional[List[str]] = None) -> Optional[Dict[str, Any]]:
|
|
2020
|
+
"""Add task to team's shared list."""
|
|
2021
|
+
try:
|
|
2022
|
+
async with self.get_session() as session:
|
|
2023
|
+
task = TeamTask(
|
|
2024
|
+
id=task_id, team_id=team_id, title=title, description=description,
|
|
2025
|
+
priority=priority, created_by=created_by,
|
|
2026
|
+
depends_on={"task_ids": depends_on} if depends_on else None,
|
|
2027
|
+
created_at=datetime.now(timezone.utc)
|
|
2028
|
+
)
|
|
2029
|
+
session.add(task)
|
|
2030
|
+
await session.commit()
|
|
2031
|
+
return {"id": task.id, "title": title, "status": "pending"}
|
|
2032
|
+
except Exception as e:
|
|
2033
|
+
logger.error(f"Failed to add team task: {e}")
|
|
2034
|
+
return None
|
|
2035
|
+
|
|
2036
|
+
async def get_team_tasks(self, team_id: str, status: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
2037
|
+
"""Get team tasks, optionally filtered by status."""
|
|
2038
|
+
try:
|
|
2039
|
+
async with self.get_session() as session:
|
|
2040
|
+
query = select(TeamTask).where(TeamTask.team_id == team_id)
|
|
2041
|
+
if status:
|
|
2042
|
+
query = query.where(TeamTask.status == status)
|
|
2043
|
+
query = query.order_by(TeamTask.priority.asc(), TeamTask.created_at.asc())
|
|
2044
|
+
result = await session.execute(query)
|
|
2045
|
+
return [
|
|
2046
|
+
{"id": t.id, "title": t.title, "description": t.description, "status": t.status,
|
|
2047
|
+
"priority": t.priority, "assigned_to": t.assigned_to, "progress": t.progress,
|
|
2048
|
+
"depends_on": t.depends_on.get("task_ids", []) if t.depends_on else []}
|
|
2049
|
+
for t in result.scalars().all()
|
|
2050
|
+
]
|
|
2051
|
+
except Exception as e:
|
|
2052
|
+
logger.error(f"Failed to get team tasks: {e}")
|
|
2053
|
+
return []
|
|
2054
|
+
|
|
2055
|
+
async def claim_task(self, task_id: str, agent_node_id: str) -> Optional[Dict[str, Any]]:
|
|
2056
|
+
"""Claim a pending task. Returns None if already claimed."""
|
|
2057
|
+
try:
|
|
2058
|
+
async with self.get_session() as session:
|
|
2059
|
+
result = await session.execute(
|
|
2060
|
+
select(TeamTask).where(
|
|
2061
|
+
TeamTask.id == task_id, TeamTask.status == "pending", TeamTask.assigned_to.is_(None)
|
|
2062
|
+
)
|
|
2063
|
+
)
|
|
2064
|
+
task = result.scalar_one_or_none()
|
|
2065
|
+
if not task:
|
|
2066
|
+
return None
|
|
2067
|
+
task.assigned_to = agent_node_id
|
|
2068
|
+
task.status = "in_progress"
|
|
2069
|
+
task.started_at = datetime.now(timezone.utc)
|
|
2070
|
+
await session.commit()
|
|
2071
|
+
return {"id": task.id, "title": task.title, "assigned_to": agent_node_id}
|
|
2072
|
+
except Exception as e:
|
|
2073
|
+
logger.error(f"Failed to claim task: {e}")
|
|
2074
|
+
return None
|
|
2075
|
+
|
|
2076
|
+
async def complete_task(self, task_id: str, result_data: Optional[Dict[str, Any]] = None) -> bool:
|
|
2077
|
+
"""Mark task as completed."""
|
|
2078
|
+
try:
|
|
2079
|
+
async with self.get_session() as session:
|
|
2080
|
+
result = await session.execute(select(TeamTask).where(TeamTask.id == task_id))
|
|
2081
|
+
task = result.scalar_one_or_none()
|
|
2082
|
+
if not task:
|
|
2083
|
+
return False
|
|
2084
|
+
task.status = "completed"
|
|
2085
|
+
task.result = result_data
|
|
2086
|
+
task.progress = 100
|
|
2087
|
+
task.completed_at = datetime.now(timezone.utc)
|
|
2088
|
+
await session.commit()
|
|
2089
|
+
return True
|
|
2090
|
+
except Exception as e:
|
|
2091
|
+
logger.error(f"Failed to complete task: {e}")
|
|
2092
|
+
return False
|
|
2093
|
+
|
|
2094
|
+
async def fail_task(self, task_id: str, error: str) -> bool:
|
|
2095
|
+
"""Mark task as failed."""
|
|
2096
|
+
try:
|
|
2097
|
+
async with self.get_session() as session:
|
|
2098
|
+
result = await session.execute(select(TeamTask).where(TeamTask.id == task_id))
|
|
2099
|
+
task = result.scalar_one_or_none()
|
|
2100
|
+
if not task:
|
|
2101
|
+
return False
|
|
2102
|
+
task.error = error
|
|
2103
|
+
task.retry_count += 1
|
|
2104
|
+
if task.retry_count < task.max_retries:
|
|
2105
|
+
task.status = "pending"
|
|
2106
|
+
task.assigned_to = None
|
|
2107
|
+
else:
|
|
2108
|
+
task.status = "failed"
|
|
2109
|
+
task.completed_at = datetime.now(timezone.utc)
|
|
2110
|
+
await session.commit()
|
|
2111
|
+
return True
|
|
2112
|
+
except Exception as e:
|
|
2113
|
+
logger.error(f"Failed to fail task: {e}")
|
|
2114
|
+
return False
|
|
2115
|
+
|
|
2116
|
+
async def get_claimable_tasks(self, team_id: str) -> List[Dict[str, Any]]:
|
|
2117
|
+
"""Get pending tasks with resolved dependencies."""
|
|
2118
|
+
try:
|
|
2119
|
+
async with self.get_session() as session:
|
|
2120
|
+
result = await session.execute(select(TeamTask).where(TeamTask.team_id == team_id))
|
|
2121
|
+
tasks = result.scalars().all()
|
|
2122
|
+
completed_ids = {t.id for t in tasks if t.status == "completed"}
|
|
2123
|
+
claimable = []
|
|
2124
|
+
for t in tasks:
|
|
2125
|
+
if t.status != "pending" or t.assigned_to:
|
|
2126
|
+
continue
|
|
2127
|
+
deps = t.depends_on.get("task_ids", []) if t.depends_on else []
|
|
2128
|
+
if all(d in completed_ids for d in deps):
|
|
2129
|
+
claimable.append({"id": t.id, "title": t.title, "priority": t.priority})
|
|
2130
|
+
return claimable
|
|
2131
|
+
except Exception as e:
|
|
2132
|
+
logger.error(f"Failed to get claimable tasks: {e}")
|
|
2133
|
+
return []
|
|
2134
|
+
|
|
2135
|
+
async def add_agent_message(self, team_id: str, from_agent: str, content: str,
|
|
2136
|
+
message_type: str = "direct", to_agent: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
|
2137
|
+
"""Add message between agents."""
|
|
2138
|
+
try:
|
|
2139
|
+
async with self.get_session() as session:
|
|
2140
|
+
msg = AgentMessage(
|
|
2141
|
+
team_id=team_id, from_agent=from_agent, to_agent=to_agent,
|
|
2142
|
+
message_type=message_type, content=content, created_at=datetime.now(timezone.utc)
|
|
2143
|
+
)
|
|
2144
|
+
session.add(msg)
|
|
2145
|
+
await session.commit()
|
|
2146
|
+
return {"id": msg.id, "from_agent": from_agent, "to_agent": to_agent}
|
|
2147
|
+
except Exception as e:
|
|
2148
|
+
logger.error(f"Failed to add agent message: {e}")
|
|
2149
|
+
return None
|
|
2150
|
+
|
|
2151
|
+
async def get_agent_messages(self, team_id: str, agent_node_id: Optional[str] = None,
|
|
2152
|
+
unread_only: bool = False) -> List[Dict[str, Any]]:
|
|
2153
|
+
"""Get messages for team or specific agent."""
|
|
2154
|
+
try:
|
|
2155
|
+
async with self.get_session() as session:
|
|
2156
|
+
from sqlalchemy import or_
|
|
2157
|
+
query = select(AgentMessage).where(AgentMessage.team_id == team_id)
|
|
2158
|
+
if agent_node_id:
|
|
2159
|
+
query = query.where(or_(
|
|
2160
|
+
AgentMessage.to_agent == agent_node_id,
|
|
2161
|
+
AgentMessage.to_agent.is_(None)
|
|
2162
|
+
))
|
|
2163
|
+
if unread_only:
|
|
2164
|
+
query = query.where(AgentMessage.read == False)
|
|
2165
|
+
query = query.order_by(AgentMessage.created_at.asc()).limit(100)
|
|
2166
|
+
result = await session.execute(query)
|
|
2167
|
+
return [
|
|
2168
|
+
{"id": m.id, "from_agent": m.from_agent, "to_agent": m.to_agent,
|
|
2169
|
+
"message_type": m.message_type, "content": m.content, "read": m.read,
|
|
2170
|
+
"created_at": m.created_at.isoformat() if m.created_at else None}
|
|
2171
|
+
for m in result.scalars().all()
|
|
2172
|
+
]
|
|
2173
|
+
except Exception as e:
|
|
2174
|
+
logger.error(f"Failed to get agent messages: {e}")
|
|
2175
|
+
return []
|
|
2176
|
+
|
|
2177
|
+
async def mark_messages_read(self, team_id: str, agent_node_id: str) -> int:
|
|
2178
|
+
"""Mark all messages as read for an agent."""
|
|
2179
|
+
try:
|
|
2180
|
+
async with self.get_session() as session:
|
|
2181
|
+
from sqlalchemy import or_
|
|
2182
|
+
result = await session.execute(
|
|
2183
|
+
select(AgentMessage).where(
|
|
2184
|
+
AgentMessage.team_id == team_id,
|
|
2185
|
+
AgentMessage.read == False,
|
|
2186
|
+
or_(AgentMessage.to_agent == agent_node_id, AgentMessage.to_agent.is_(None))
|
|
2187
|
+
)
|
|
2188
|
+
)
|
|
2189
|
+
messages = result.scalars().all()
|
|
2190
|
+
for m in messages:
|
|
2191
|
+
m.read = True
|
|
2192
|
+
await session.commit()
|
|
2193
|
+
return len(messages)
|
|
2194
|
+
except Exception as e:
|
|
2195
|
+
logger.error(f"Failed to mark messages read: {e}")
|
|
2196
|
+
return 0
|
|
2197
|
+
|
|
2198
|
+
async def get_team_stats(self, team_id: str) -> Dict[str, Any]:
|
|
2199
|
+
"""Get team statistics."""
|
|
2200
|
+
try:
|
|
2201
|
+
async with self.get_session() as session:
|
|
2202
|
+
# Get counts in simple queries
|
|
2203
|
+
team_result = await session.execute(select(AgentTeam).where(AgentTeam.id == team_id))
|
|
2204
|
+
team = team_result.scalar_one_or_none()
|
|
2205
|
+
if not team:
|
|
2206
|
+
return {"error": "Team not found"}
|
|
2207
|
+
|
|
2208
|
+
members_result = await session.execute(select(TeamMember).where(TeamMember.team_id == team_id))
|
|
2209
|
+
members = members_result.scalars().all()
|
|
2210
|
+
|
|
2211
|
+
tasks_result = await session.execute(select(TeamTask).where(TeamTask.team_id == team_id))
|
|
2212
|
+
tasks = tasks_result.scalars().all()
|
|
2213
|
+
|
|
2214
|
+
return {
|
|
2215
|
+
"team_id": team_id,
|
|
2216
|
+
"status": team.status,
|
|
2217
|
+
"member_count": len(members),
|
|
2218
|
+
"task_total": len(tasks),
|
|
2219
|
+
"task_pending": sum(1 for t in tasks if t.status == "pending"),
|
|
2220
|
+
"task_in_progress": sum(1 for t in tasks if t.status == "in_progress"),
|
|
2221
|
+
"task_completed": sum(1 for t in tasks if t.status == "completed"),
|
|
2222
|
+
"task_failed": sum(1 for t in tasks if t.status == "failed"),
|
|
2223
|
+
"members": [{"id": m.agent_node_id, "type": m.agent_type, "status": m.status} for m in members],
|
|
2224
|
+
"active_tasks": [{"id": t.id, "title": t.title, "assigned_to": t.assigned_to, "progress": t.progress}
|
|
2225
|
+
for t in tasks if t.status == "in_progress"]
|
|
2226
|
+
}
|
|
2227
|
+
except Exception as e:
|
|
2228
|
+
logger.error(f"Failed to get team stats: {e}")
|
|
2229
|
+
return {"error": str(e)}
|
package/server/main.py
CHANGED
|
@@ -27,7 +27,7 @@ from fastapi.responses import ORJSONResponse
|
|
|
27
27
|
from core.container import container
|
|
28
28
|
from core.config import Settings
|
|
29
29
|
from core.logging import configure_logging, get_logger, setup_websocket_logging, shutdown_websocket_logging
|
|
30
|
-
from routers import workflow, database, maps, nodejs_compat, android, websocket, webhook, auth
|
|
30
|
+
from routers import workflow, database, maps, nodejs_compat, android, websocket, webhook, auth, twitter
|
|
31
31
|
|
|
32
32
|
# Initialize settings and logging
|
|
33
33
|
settings = Settings()
|
|
@@ -61,6 +61,7 @@ async def lifespan(app: FastAPI):
|
|
|
61
61
|
"routers.websocket",
|
|
62
62
|
"routers.webhook",
|
|
63
63
|
"routers.auth",
|
|
64
|
+
"routers.twitter",
|
|
64
65
|
"middleware.auth"
|
|
65
66
|
])
|
|
66
67
|
|
|
@@ -122,6 +123,12 @@ async def lifespan(app: FastAPI):
|
|
|
122
123
|
compaction_svc.set_ai_service(container.ai_service())
|
|
123
124
|
logger.info("Compaction service initialized")
|
|
124
125
|
|
|
126
|
+
# Initialize agent team service
|
|
127
|
+
from services.agent_team import init_agent_team_service
|
|
128
|
+
from services.status_broadcaster import get_status_broadcaster
|
|
129
|
+
init_agent_team_service(container.database(), get_status_broadcaster())
|
|
130
|
+
logger.info("Agent team service initialized")
|
|
131
|
+
|
|
125
132
|
# Record startup time for health reporting
|
|
126
133
|
set_startup_time()
|
|
127
134
|
|
|
@@ -268,6 +275,7 @@ app.include_router(maps.router)
|
|
|
268
275
|
app.include_router(android.router)
|
|
269
276
|
app.include_router(websocket.router)
|
|
270
277
|
app.include_router(webhook.router)
|
|
278
|
+
app.include_router(twitter.router) # Twitter/X OAuth routes
|
|
271
279
|
|
|
272
280
|
|
|
273
281
|
@app.get("/health")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""SQLModel database models and tables."""
|
|
2
2
|
|
|
3
3
|
from datetime import datetime, timezone
|
|
4
|
-
from typing import Optional, Dict, Any
|
|
4
|
+
from typing import Optional, Dict, Any, List
|
|
5
5
|
from sqlmodel import SQLModel, Field, Column, DateTime, JSON
|
|
6
6
|
from sqlalchemy import func
|
|
7
7
|
|
|
@@ -315,6 +315,11 @@ class TokenUsageMetric(SQLModel, table=True):
|
|
|
315
315
|
iteration: int = Field(default=1)
|
|
316
316
|
execution_id: Optional[str] = Field(default=None, max_length=255)
|
|
317
317
|
created_at: Optional[datetime] = Field(default=None)
|
|
318
|
+
# Cost fields (USD)
|
|
319
|
+
input_cost: float = Field(default=0.0)
|
|
320
|
+
output_cost: float = Field(default=0.0)
|
|
321
|
+
cache_cost: float = Field(default=0.0)
|
|
322
|
+
total_cost: float = Field(default=0.0)
|
|
318
323
|
|
|
319
324
|
|
|
320
325
|
class CompactionEvent(SQLModel, table=True):
|
|
@@ -356,4 +361,109 @@ class SessionTokenState(SQLModel, table=True):
|
|
|
356
361
|
compaction_count: int = Field(default=0)
|
|
357
362
|
custom_threshold: Optional[int] = Field(default=None)
|
|
358
363
|
compaction_enabled: bool = Field(default=True)
|
|
359
|
-
updated_at: Optional[datetime] = Field(default=None)
|
|
364
|
+
updated_at: Optional[datetime] = Field(default=None)
|
|
365
|
+
# Cumulative cost fields (USD)
|
|
366
|
+
cumulative_input_cost: float = Field(default=0.0)
|
|
367
|
+
cumulative_output_cost: float = Field(default=0.0)
|
|
368
|
+
cumulative_total_cost: float = Field(default=0.0)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# =============================================================================
|
|
372
|
+
# Agent Teams - Claude SDK Agent Teams Pattern
|
|
373
|
+
# =============================================================================
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class AgentTeam(SQLModel, table=True):
|
|
377
|
+
"""Agent team for multi-agent collaboration.
|
|
378
|
+
|
|
379
|
+
Implements Claude SDK Agent Teams pattern where a team lead coordinates
|
|
380
|
+
multiple teammate agents working on shared tasks.
|
|
381
|
+
"""
|
|
382
|
+
|
|
383
|
+
__tablename__ = "agent_teams"
|
|
384
|
+
|
|
385
|
+
id: str = Field(primary_key=True, max_length=255)
|
|
386
|
+
workflow_id: str = Field(index=True, max_length=255)
|
|
387
|
+
team_lead_node_id: str = Field(max_length=255)
|
|
388
|
+
status: str = Field(default="active", max_length=20) # active, completed, failed, dissolved
|
|
389
|
+
config: Dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON))
|
|
390
|
+
created_at: datetime = Field(
|
|
391
|
+
default_factory=lambda: datetime.now(timezone.utc),
|
|
392
|
+
sa_column=Column(DateTime(timezone=True), server_default=func.now())
|
|
393
|
+
)
|
|
394
|
+
completed_at: Optional[datetime] = Field(default=None)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class TeamMember(SQLModel, table=True):
|
|
398
|
+
"""Agent team membership.
|
|
399
|
+
|
|
400
|
+
Tracks which agents belong to which team and their current status.
|
|
401
|
+
"""
|
|
402
|
+
|
|
403
|
+
__tablename__ = "team_members"
|
|
404
|
+
|
|
405
|
+
id: Optional[int] = Field(default=None, primary_key=True)
|
|
406
|
+
team_id: str = Field(index=True, max_length=255)
|
|
407
|
+
agent_node_id: str = Field(index=True, max_length=255)
|
|
408
|
+
agent_type: str = Field(max_length=100) # orchestrator_agent, android_agent, etc.
|
|
409
|
+
agent_label: Optional[str] = Field(default=None, max_length=255) # User-defined label
|
|
410
|
+
role: str = Field(default="teammate", max_length=20) # team_lead, teammate
|
|
411
|
+
status: str = Field(default="idle", max_length=20) # idle, working, offline
|
|
412
|
+
capabilities: Optional[Dict[str, Any]] = Field(default=None, sa_column=Column(JSON))
|
|
413
|
+
joined_at: datetime = Field(
|
|
414
|
+
default_factory=lambda: datetime.now(timezone.utc),
|
|
415
|
+
sa_column=Column(DateTime(timezone=True), server_default=func.now())
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
class TeamTask(SQLModel, table=True):
|
|
420
|
+
"""Shared task in agent team task list.
|
|
421
|
+
|
|
422
|
+
Tasks are created by the team lead and claimed by teammates.
|
|
423
|
+
Supports dependencies between tasks.
|
|
424
|
+
"""
|
|
425
|
+
|
|
426
|
+
__tablename__ = "team_tasks"
|
|
427
|
+
|
|
428
|
+
id: str = Field(primary_key=True, max_length=255)
|
|
429
|
+
team_id: str = Field(index=True, max_length=255)
|
|
430
|
+
title: str = Field(max_length=500)
|
|
431
|
+
description: Optional[str] = Field(default=None, max_length=5000)
|
|
432
|
+
status: str = Field(default="pending", max_length=20) # pending, in_progress, completed, failed, skipped
|
|
433
|
+
priority: int = Field(default=3) # 1-5, lower = higher priority
|
|
434
|
+
created_by: str = Field(max_length=255) # agent_node_id
|
|
435
|
+
assigned_to: Optional[str] = Field(default=None, max_length=255)
|
|
436
|
+
depends_on: Optional[Dict[str, Any]] = Field(default=None, sa_column=Column(JSON)) # List of task_ids
|
|
437
|
+
result: Optional[Dict[str, Any]] = Field(default=None, sa_column=Column(JSON))
|
|
438
|
+
error: Optional[str] = Field(default=None, max_length=2000)
|
|
439
|
+
retry_count: int = Field(default=0)
|
|
440
|
+
max_retries: int = Field(default=3)
|
|
441
|
+
progress: int = Field(default=0) # 0-100 percentage
|
|
442
|
+
created_at: datetime = Field(
|
|
443
|
+
default_factory=lambda: datetime.now(timezone.utc),
|
|
444
|
+
sa_column=Column(DateTime(timezone=True), server_default=func.now())
|
|
445
|
+
)
|
|
446
|
+
started_at: Optional[datetime] = Field(default=None)
|
|
447
|
+
completed_at: Optional[datetime] = Field(default=None)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class AgentMessage(SQLModel, table=True):
|
|
451
|
+
"""Inter-agent messages within a team.
|
|
452
|
+
|
|
453
|
+
Supports direct messages between agents and broadcasts to all team members.
|
|
454
|
+
"""
|
|
455
|
+
|
|
456
|
+
__tablename__ = "agent_messages"
|
|
457
|
+
|
|
458
|
+
id: Optional[int] = Field(default=None, primary_key=True)
|
|
459
|
+
team_id: str = Field(index=True, max_length=255)
|
|
460
|
+
from_agent: str = Field(max_length=255) # node_id
|
|
461
|
+
to_agent: Optional[str] = Field(default=None, max_length=255) # None = broadcast
|
|
462
|
+
message_type: str = Field(max_length=50) # direct, broadcast, task_assignment, task_update, task_complete
|
|
463
|
+
content: str = Field(max_length=10000)
|
|
464
|
+
extra_data: Optional[Dict[str, Any]] = Field(default=None, sa_column=Column(JSON))
|
|
465
|
+
read: bool = Field(default=False)
|
|
466
|
+
created_at: datetime = Field(
|
|
467
|
+
default_factory=lambda: datetime.now(timezone.utc),
|
|
468
|
+
sa_column=Column(DateTime(timezone=True), server_default=func.now())
|
|
469
|
+
)
|
package/server/pyproject.toml
CHANGED