kairo-code 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. kairo/backend/api/agents.py +337 -16
  2. kairo/backend/app.py +84 -4
  3. kairo/backend/config.py +4 -2
  4. kairo/backend/models/agent.py +216 -2
  5. kairo/backend/models/api_key.py +4 -1
  6. kairo/backend/models/task.py +31 -0
  7. kairo/backend/models/user_provider_key.py +26 -0
  8. kairo/backend/schemas/agent.py +249 -2
  9. kairo/backend/schemas/api_key.py +3 -0
  10. kairo/backend/services/agent/__init__.py +52 -0
  11. kairo/backend/services/agent/agent_alerts_evaluation_service.py +224 -0
  12. kairo/backend/services/agent/agent_alerts_service.py +201 -0
  13. kairo/backend/services/agent/agent_commands_service.py +142 -0
  14. kairo/backend/services/agent/agent_crud_service.py +150 -0
  15. kairo/backend/services/agent/agent_events_service.py +103 -0
  16. kairo/backend/services/agent/agent_heartbeat_service.py +207 -0
  17. kairo/backend/services/agent/agent_metrics_rollup_service.py +248 -0
  18. kairo/backend/services/agent/agent_metrics_service.py +259 -0
  19. kairo/backend/services/agent/agent_service.py +315 -0
  20. kairo/backend/services/agent/agent_setup_service.py +180 -0
  21. kairo/backend/services/agent/constants.py +28 -0
  22. kairo/backend/services/agent_service.py +18 -102
  23. kairo/backend/services/api_key_service.py +23 -3
  24. kairo/backend/services/byok_service.py +204 -0
  25. kairo/backend/services/chat_service.py +398 -63
  26. kairo/backend/services/deep_search_service.py +159 -0
  27. kairo/backend/services/email_service.py +418 -19
  28. kairo/backend/services/few_shot_service.py +223 -0
  29. kairo/backend/services/post_processor.py +261 -0
  30. kairo/backend/services/rag_service.py +150 -0
  31. kairo/backend/services/task_service.py +119 -0
  32. kairo/backend/tests/__init__.py +1 -0
  33. kairo/backend/tests/e2e/__init__.py +1 -0
  34. kairo/backend/tests/e2e/agents/__init__.py +1 -0
  35. kairo/backend/tests/e2e/agents/conftest.py +389 -0
  36. kairo/backend/tests/e2e/agents/test_agent_alerts.py +802 -0
  37. kairo/backend/tests/e2e/agents/test_agent_commands.py +456 -0
  38. kairo/backend/tests/e2e/agents/test_agent_crud.py +455 -0
  39. kairo/backend/tests/e2e/agents/test_agent_events.py +415 -0
  40. kairo/backend/tests/e2e/agents/test_agent_heartbeat.py +520 -0
  41. kairo/backend/tests/e2e/agents/test_agent_metrics.py +587 -0
  42. kairo/backend/tests/e2e/agents/test_agent_setup.py +349 -0
  43. kairo/migrations/versions/010_agent_dashboard.py +246 -0
  44. {kairo_code-0.1.0.dist-info → kairo_code-0.2.0.dist-info}/METADATA +1 -1
  45. {kairo_code-0.1.0.dist-info → kairo_code-0.2.0.dist-info}/RECORD +50 -16
  46. {kairo_code-0.1.0.dist-info → kairo_code-0.2.0.dist-info}/top_level.txt +1 -0
  47. kairo_migrations/env.py +92 -0
  48. kairo_migrations/versions/001_add_agent_dashboard_extensions.py +450 -0
  49. {kairo_code-0.1.0.dist-info → kairo_code-0.2.0.dist-info}/WHEEL +0 -0
  50. {kairo_code-0.1.0.dist-info → kairo_code-0.2.0.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,9 @@
1
1
  import uuid
2
2
  from datetime import datetime, UTC
3
+ from typing import Any
3
4
 
4
- from sqlalchemy import DateTime, ForeignKey, String, Text
5
+ from sqlalchemy import BigInteger, Boolean, DateTime, Float, ForeignKey, Integer, String, Text, Date
6
+ from sqlalchemy.dialects.postgresql import JSONB
5
7
  from sqlalchemy.orm import Mapped, mapped_column
6
8
 
7
9
  from backend.core.database import Base
@@ -20,11 +22,223 @@ class Agent(Base):
20
22
  system_prompt: Mapped[str | None] = mapped_column(Text, nullable=True)
21
23
  model_preference: Mapped[str] = mapped_column(String, default="nyx")
22
24
  tools_config: Mapped[str | None] = mapped_column(Text, nullable=True)
23
- status: Mapped[str] = mapped_column(String, default="offline")
25
+
26
+ # State machine: created -> online -> idle/busy -> offline -> stale -> disabled
27
+ state: Mapped[str] = mapped_column(String, default="created")
28
+ status: Mapped[str] = mapped_column(String, default="offline") # Legacy, maps to state
29
+ agent_type: Mapped[str] = mapped_column(String, default="sdk") # sdk or container
30
+
31
+ # Connection info
32
+ sdk_version: Mapped[str | None] = mapped_column(String, nullable=True)
33
+ host_info: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
24
34
  last_heartbeat_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
35
+ last_online_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
36
+ first_connected_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
37
+
38
+ # Restart tracking
39
+ restart_count: Mapped[int] = mapped_column(Integer, default=0)
40
+ last_restart_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
41
+ last_restart_reason: Mapped[str | None] = mapped_column(String, nullable=True)
42
+
43
+ # Error tracking
44
+ last_error_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
45
+ last_error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
46
+
47
+ # Soft delete
48
+ deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
49
+
25
50
  created_at: Mapped[datetime] = mapped_column(
26
51
  DateTime(timezone=True), default=lambda: datetime.now(UTC)
27
52
  )
28
53
  updated_at: Mapped[datetime] = mapped_column(
29
54
  DateTime(timezone=True), default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC)
30
55
  )
56
+
57
+
58
+ class AgentMetrics1m(Base):
59
+ """1-minute granularity metrics (30 day retention)"""
60
+ __tablename__ = "agent_metrics_1m"
61
+
62
+ id: Mapped[str] = mapped_column(primary_key=True, default=lambda: str(uuid.uuid4()))
63
+ agent_id: Mapped[str] = mapped_column(ForeignKey("agents.id", ondelete="CASCADE"))
64
+ bucket_time: Mapped[datetime] = mapped_column(DateTime(timezone=True))
65
+
66
+ request_count: Mapped[int] = mapped_column(Integer, default=0)
67
+ error_count: Mapped[int] = mapped_column(Integer, default=0)
68
+ timeout_count: Mapped[int] = mapped_column(Integer, default=0)
69
+ input_tokens: Mapped[int] = mapped_column(BigInteger, default=0)
70
+ output_tokens: Mapped[int] = mapped_column(BigInteger, default=0)
71
+ total_latency_ms: Mapped[int] = mapped_column(BigInteger, default=0)
72
+ min_latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
73
+ max_latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
74
+ tool_calls: Mapped[int] = mapped_column(Integer, default=0)
75
+ tool_errors: Mapped[int] = mapped_column(Integer, default=0)
76
+ tool_breakdown: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
77
+ model_breakdown: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
78
+
79
+ created_at: Mapped[datetime] = mapped_column(
80
+ DateTime(timezone=True), default=lambda: datetime.now(UTC)
81
+ )
82
+
83
+
84
+ class AgentMetrics1h(Base):
85
+ """1-hour granularity metrics (1 year retention)"""
86
+ __tablename__ = "agent_metrics_1h"
87
+
88
+ id: Mapped[str] = mapped_column(primary_key=True, default=lambda: str(uuid.uuid4()))
89
+ agent_id: Mapped[str] = mapped_column(ForeignKey("agents.id", ondelete="CASCADE"))
90
+ bucket_time: Mapped[datetime] = mapped_column(DateTime(timezone=True))
91
+
92
+ request_count: Mapped[int] = mapped_column(Integer, default=0)
93
+ error_count: Mapped[int] = mapped_column(Integer, default=0)
94
+ timeout_count: Mapped[int] = mapped_column(Integer, default=0)
95
+ input_tokens: Mapped[int] = mapped_column(BigInteger, default=0)
96
+ output_tokens: Mapped[int] = mapped_column(BigInteger, default=0)
97
+ total_latency_ms: Mapped[int] = mapped_column(BigInteger, default=0)
98
+ min_latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
99
+ max_latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
100
+ p50_latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
101
+ p99_latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
102
+ tool_calls: Mapped[int] = mapped_column(Integer, default=0)
103
+ tool_errors: Mapped[int] = mapped_column(Integer, default=0)
104
+ tool_breakdown: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
105
+ model_breakdown: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
106
+
107
+ created_at: Mapped[datetime] = mapped_column(
108
+ DateTime(timezone=True), default=lambda: datetime.now(UTC)
109
+ )
110
+
111
+
112
+ class AgentMetricsDaily(Base):
113
+ """Daily granularity metrics (2 year retention)"""
114
+ __tablename__ = "agent_metrics_daily"
115
+
116
+ id: Mapped[str] = mapped_column(primary_key=True, default=lambda: str(uuid.uuid4()))
117
+ agent_id: Mapped[str] = mapped_column(ForeignKey("agents.id", ondelete="CASCADE"))
118
+ date: Mapped[datetime] = mapped_column(Date)
119
+
120
+ request_count: Mapped[int] = mapped_column(Integer, default=0)
121
+ error_count: Mapped[int] = mapped_column(Integer, default=0)
122
+ timeout_count: Mapped[int] = mapped_column(Integer, default=0)
123
+ input_tokens: Mapped[int] = mapped_column(BigInteger, default=0)
124
+ output_tokens: Mapped[int] = mapped_column(BigInteger, default=0)
125
+ total_latency_ms: Mapped[int] = mapped_column(BigInteger, default=0)
126
+ min_latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
127
+ max_latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
128
+ avg_latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
129
+ p50_latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
130
+ p99_latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
131
+ tool_calls: Mapped[int] = mapped_column(Integer, default=0)
132
+ tool_errors: Mapped[int] = mapped_column(Integer, default=0)
133
+ tool_breakdown: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
134
+ model_breakdown: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
135
+ uptime_seconds: Mapped[int] = mapped_column(Integer, default=0)
136
+ downtime_seconds: Mapped[int] = mapped_column(Integer, default=0)
137
+
138
+ created_at: Mapped[datetime] = mapped_column(
139
+ DateTime(timezone=True), default=lambda: datetime.now(UTC)
140
+ )
141
+
142
+
143
+ class AgentEvent(Base):
144
+ """Agent events log"""
145
+ __tablename__ = "agent_events"
146
+
147
+ id: Mapped[str] = mapped_column(primary_key=True, default=lambda: str(uuid.uuid4()))
148
+ agent_id: Mapped[str] = mapped_column(ForeignKey("agents.id", ondelete="CASCADE"))
149
+ event_type: Mapped[str] = mapped_column(String) # heartbeat, connection, error, restart, config_change
150
+ event_data: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
151
+ error_type: Mapped[str | None] = mapped_column(String, nullable=True)
152
+ error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
153
+ session_id: Mapped[str | None] = mapped_column(String, nullable=True)
154
+ client_ip: Mapped[str | None] = mapped_column(String, nullable=True)
155
+
156
+ created_at: Mapped[datetime] = mapped_column(
157
+ DateTime(timezone=True), default=lambda: datetime.now(UTC)
158
+ )
159
+
160
+
161
+ class AgentAlertConfig(Base):
162
+ """Alert configuration per agent"""
163
+ __tablename__ = "agent_alert_configs"
164
+
165
+ id: Mapped[str] = mapped_column(primary_key=True, default=lambda: str(uuid.uuid4()))
166
+ agent_id: Mapped[str] = mapped_column(ForeignKey("agents.id", ondelete="CASCADE"))
167
+ user_id: Mapped[str] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
168
+ name: Mapped[str] = mapped_column(String)
169
+ alert_type: Mapped[str] = mapped_column(String) # offline, error_rate, latency, token_budget
170
+ metric: Mapped[str] = mapped_column(String)
171
+ condition: Mapped[str] = mapped_column(String) # gt, lt, gte, lte, eq
172
+ threshold: Mapped[float] = mapped_column(Float)
173
+ window_seconds: Mapped[int] = mapped_column(Integer, default=300)
174
+ cooldown_seconds: Mapped[int] = mapped_column(Integer, default=3600)
175
+ severity: Mapped[str] = mapped_column(String, default="warning")
176
+ channels: Mapped[list] = mapped_column(JSONB, default=lambda: ["email"])
177
+ is_enabled: Mapped[bool] = mapped_column(Boolean, default=True)
178
+ last_triggered_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
179
+
180
+ created_at: Mapped[datetime] = mapped_column(
181
+ DateTime(timezone=True), default=lambda: datetime.now(UTC)
182
+ )
183
+ updated_at: Mapped[datetime] = mapped_column(
184
+ DateTime(timezone=True), default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC)
185
+ )
186
+
187
+
188
+ class AgentAlertHistory(Base):
189
+ """Alert history/incidents"""
190
+ __tablename__ = "agent_alert_history"
191
+
192
+ id: Mapped[str] = mapped_column(primary_key=True, default=lambda: str(uuid.uuid4()))
193
+ config_id: Mapped[str] = mapped_column(ForeignKey("agent_alert_configs.id", ondelete="CASCADE"))
194
+ agent_id: Mapped[str] = mapped_column(ForeignKey("agents.id", ondelete="CASCADE"))
195
+ alert_type: Mapped[str] = mapped_column(String)
196
+ severity: Mapped[str] = mapped_column(String)
197
+ status: Mapped[str] = mapped_column(String, default="triggered") # triggered, acknowledged, resolved
198
+ message: Mapped[str | None] = mapped_column(Text, nullable=True)
199
+ trigger_value: Mapped[float | None] = mapped_column(Float, nullable=True)
200
+ threshold_value: Mapped[float | None] = mapped_column(Float, nullable=True)
201
+ acknowledged_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
202
+ acknowledged_by: Mapped[str | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
203
+ resolved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
204
+
205
+ created_at: Mapped[datetime] = mapped_column(
206
+ DateTime(timezone=True), default=lambda: datetime.now(UTC)
207
+ )
208
+
209
+
210
+ class AgentSetupToken(Base):
211
+ """One-time setup tokens for guided deployment wizard"""
212
+ __tablename__ = "agent_setup_tokens"
213
+
214
+ id: Mapped[str] = mapped_column(primary_key=True, default=lambda: str(uuid.uuid4()))
215
+ agent_id: Mapped[str] = mapped_column(ForeignKey("agents.id", ondelete="CASCADE"))
216
+ user_id: Mapped[str] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
217
+ token_hash: Mapped[str] = mapped_column(String, unique=True)
218
+ expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
219
+ used: Mapped[bool] = mapped_column(Boolean, default=False)
220
+ used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
221
+ used_from_ip: Mapped[str | None] = mapped_column(String, nullable=True)
222
+
223
+ created_at: Mapped[datetime] = mapped_column(
224
+ DateTime(timezone=True), default=lambda: datetime.now(UTC)
225
+ )
226
+
227
+
228
+ class AgentCommand(Base):
229
+ """Pending commands for agents (delivered via heartbeat)"""
230
+ __tablename__ = "agent_commands"
231
+
232
+ id: Mapped[str] = mapped_column(primary_key=True, default=lambda: str(uuid.uuid4()))
233
+ agent_id: Mapped[str] = mapped_column(ForeignKey("agents.id", ondelete="CASCADE"))
234
+ command_type: Mapped[str] = mapped_column(String) # restart, stop, config_refresh
235
+ payload: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
236
+ issued_by: Mapped[str | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
237
+ status: Mapped[str] = mapped_column(String, default="pending") # pending, dispatched, acknowledged, expired
238
+ dispatched_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
239
+ acknowledged_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
240
+ expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
241
+
242
+ created_at: Mapped[datetime] = mapped_column(
243
+ DateTime(timezone=True), default=lambda: datetime.now(UTC)
244
+ )
@@ -12,6 +12,9 @@ class ApiKey(Base):
12
12
 
13
13
  id: Mapped[str] = mapped_column(primary_key=True, default=lambda: str(uuid.uuid4()))
14
14
  user_id: Mapped[str] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True)
15
+ agent_id: Mapped[str | None] = mapped_column(
16
+ ForeignKey("agents.id", ondelete="CASCADE"), nullable=True, index=True
17
+ )
15
18
  name: Mapped[str] = mapped_column(String)
16
19
  key_prefix: Mapped[str] = mapped_column(String, index=True)
17
20
  key_hash: Mapped[str] = mapped_column(String)
@@ -21,5 +24,5 @@ class ApiKey(Base):
21
24
  DateTime(timezone=True), default=lambda: datetime.now(UTC)
22
25
  )
23
26
  expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
24
- key_type: Mapped[str] = mapped_column(String, default="api")
27
+ key_type: Mapped[str] = mapped_column(String, default="api") # api, agent
25
28
  rate_limit_rpm: Mapped[int] = mapped_column(Integer, default=60)
@@ -0,0 +1,31 @@
1
+ import uuid
2
+ from datetime import datetime, UTC
3
+
4
+ from sqlalchemy import DateTime, ForeignKey, JSON, String, Text
5
+ from sqlalchemy.orm import Mapped, mapped_column
6
+
7
+ from backend.core.database import Base
8
+
9
+
10
+ class Task(Base):
11
+ __tablename__ = "tasks"
12
+
13
+ id: Mapped[str] = mapped_column(primary_key=True, default=lambda: str(uuid.uuid4()))
14
+ agent_id: Mapped[str | None] = mapped_column(
15
+ ForeignKey("agents.id", ondelete="SET NULL"), nullable=True
16
+ )
17
+ user_id: Mapped[str] = mapped_column(
18
+ ForeignKey("users.id", ondelete="CASCADE"), nullable=False
19
+ )
20
+ status: Mapped[str] = mapped_column(String, default="pending")
21
+ input: Mapped[dict] = mapped_column(JSON, nullable=False)
22
+ output: Mapped[dict | None] = mapped_column(JSON, nullable=True)
23
+ error: Mapped[str | None] = mapped_column(Text, nullable=True)
24
+ created_at: Mapped[datetime] = mapped_column(
25
+ DateTime(timezone=True), default=lambda: datetime.now(UTC)
26
+ )
27
+ updated_at: Mapped[datetime] = mapped_column(
28
+ DateTime(timezone=True), default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC)
29
+ )
30
+ claimed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
31
+ completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
@@ -0,0 +1,26 @@
1
+ import uuid
2
+ from datetime import datetime, UTC
3
+
4
+ from sqlalchemy import DateTime, ForeignKey, String, Text
5
+ from sqlalchemy.orm import Mapped, mapped_column
6
+
7
+ from backend.core.database import Base
8
+
9
+
10
+ class UserProviderKey(Base):
11
+ __tablename__ = "user_provider_keys"
12
+
13
+ id: Mapped[str] = mapped_column(primary_key=True, default=lambda: str(uuid.uuid4()))
14
+ user_id: Mapped[str] = mapped_column(
15
+ ForeignKey("users.id", ondelete="CASCADE"), index=True, nullable=False
16
+ )
17
+ provider: Mapped[str] = mapped_column(String(50), nullable=False) # "openai", "anthropic"
18
+ encrypted_key: Mapped[str] = mapped_column(Text, nullable=False) # Fernet-encrypted
19
+ key_suffix: Mapped[str] = mapped_column(String(8), nullable=False) # last 4 chars for display
20
+ label: Mapped[str | None] = mapped_column(String(100), nullable=True)
21
+ created_at: Mapped[datetime] = mapped_column(
22
+ DateTime(timezone=True), default=lambda: datetime.now(UTC)
23
+ )
24
+ updated_at: Mapped[datetime] = mapped_column(
25
+ DateTime(timezone=True), default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC)
26
+ )
@@ -1,13 +1,17 @@
1
1
  from datetime import datetime
2
+ from typing import Any
2
3
 
3
4
  from pydantic import BaseModel, Field
4
5
 
5
6
 
7
+ # ─── Agent CRUD ───────────────────────────────────────────────────────────────
8
+
6
9
  class RegisterAgentRequest(BaseModel):
7
10
  name: str = Field(..., min_length=1, max_length=100)
8
11
  description: str | None = Field(None, max_length=500)
9
12
  system_prompt: str | None = Field(None, max_length=10000)
10
13
  model_preference: str = "nyx"
14
+ agent_type: str = Field("sdk", pattern="^(sdk|container)$")
11
15
  tools: list[str] | None = None
12
16
 
13
17
 
@@ -24,19 +28,262 @@ class AgentResponse(BaseModel):
24
28
  name: str
25
29
  description: str | None = None
26
30
  model_preference: str
27
- status: str
31
+ agent_type: str
32
+ state: str
33
+ status: str # Legacy
34
+ sdk_version: str | None = None
35
+ host_info: dict | None = None
28
36
  last_heartbeat_at: datetime | None = None
37
+ last_online_at: datetime | None = None
38
+ first_connected_at: datetime | None = None
39
+ restart_count: int = 0
40
+ last_error_at: datetime | None = None
41
+ last_error_message: str | None = None
29
42
  created_at: datetime
30
43
  updated_at: datetime
31
44
 
32
45
  model_config = {"from_attributes": True}
33
46
 
34
47
 
48
+ class AgentListResponse(BaseModel):
49
+ """Agent list item with summary metrics"""
50
+ id: str
51
+ name: str
52
+ description: str | None = None
53
+ model_preference: str
54
+ agent_type: str
55
+ state: str
56
+ sdk_version: str | None = None
57
+ last_heartbeat_at: datetime | None = None
58
+ created_at: datetime
59
+ # Summary metrics (last 24h)
60
+ requests_24h: int = 0
61
+ errors_24h: int = 0
62
+ tokens_24h: int = 0
63
+ error_rate: float = 0.0
64
+
65
+ model_config = {"from_attributes": True}
66
+
67
+
68
+ # ─── Heartbeat ────────────────────────────────────────────────────────────────
69
+
70
+ class HostInfo(BaseModel):
71
+ hostname: str | None = None
72
+ ip: str | None = None
73
+ os: str | None = None
74
+ memory_mb: int | None = None
75
+ memory_used_percent: float | None = None
76
+
77
+
78
+ class ProcessInfo(BaseModel):
79
+ pid: int | None = None
80
+ uptime_seconds: int | None = None
81
+ memory_rss_mb: int | None = None
82
+ cpu_percent: float | None = None
83
+ thread_count: int | None = None
84
+
85
+
86
+ class MetricsSinceLastHeartbeat(BaseModel):
87
+ requests_completed: int = 0
88
+ requests_failed: int = 0
89
+ total_latency_ms: int = 0
90
+ input_tokens: int = 0
91
+ output_tokens: int = 0
92
+ tool_calls: int = 0
93
+ tool_errors: int = 0
94
+
95
+
35
96
  class AgentHeartbeatRequest(BaseModel):
36
97
  agent_id: str
37
- status: str = Field("online", pattern="^(online|busy|idle)$")
98
+ status: str = Field("online", pattern="^(online|busy|idle|error)$")
99
+ sdk_version: str | None = None
100
+ host_info: HostInfo | None = None
101
+ process_info: ProcessInfo | None = None
102
+ metrics_since_last_heartbeat: MetricsSinceLastHeartbeat | None = None
103
+ queue_depth: int | None = None
104
+ active_request: bool = False
105
+ last_error: dict | None = None
106
+
107
+
108
+ class AgentCommand(BaseModel):
109
+ command_id: str
110
+ type: str
111
+ payload: dict | None = None
112
+ issued_at: datetime
113
+ expires_at: datetime
114
+ signature: str
38
115
 
39
116
 
40
117
  class AgentHeartbeatResponse(BaseModel):
41
118
  acknowledged: bool
42
119
  server_time: datetime
120
+ commands: list[AgentCommand] = []
121
+
122
+
123
+ # ─── Metrics ──────────────────────────────────────────────────────────────────
124
+
125
+ class AgentMetricsQuery(BaseModel):
126
+ range: str = Field("24h", pattern="^(1h|6h|24h|7d|30d)$")
127
+ granularity: str = Field("auto", pattern="^(auto|1m|1h|1d)$")
128
+
129
+
130
+ class MetricDataPoint(BaseModel):
131
+ timestamp: datetime
132
+ request_count: int = 0
133
+ error_count: int = 0
134
+ input_tokens: int = 0
135
+ output_tokens: int = 0
136
+ avg_latency_ms: float | None = None
137
+ p99_latency_ms: int | None = None
138
+ tool_calls: int = 0
139
+
140
+
141
+ class AgentMetricsResponse(BaseModel):
142
+ agent_id: str
143
+ range: str
144
+ granularity: str
145
+ data: list[MetricDataPoint]
146
+ summary: dict
147
+
148
+
149
+ # ─── Events ───────────────────────────────────────────────────────────────────
150
+
151
+ class AgentEventResponse(BaseModel):
152
+ id: str
153
+ event_type: str
154
+ event_data: dict | None = None
155
+ error_type: str | None = None
156
+ error_message: str | None = None
157
+ client_ip: str | None = None
158
+ created_at: datetime
159
+
160
+ model_config = {"from_attributes": True}
161
+
162
+
163
+ class AgentEventsQuery(BaseModel):
164
+ event_type: str | None = None
165
+ limit: int = Field(50, ge=1, le=500)
166
+ cursor: str | None = None
167
+
168
+
169
+ # ─── Commands ─────────────────────────────────────────────────────────────────
170
+
171
+ class IssueCommandRequest(BaseModel):
172
+ command_type: str = Field(..., pattern="^(restart|stop|config_refresh)$")
173
+ payload: dict | None = None
174
+
175
+
176
+ class CommandResponse(BaseModel):
177
+ id: str
178
+ command_type: str
179
+ status: str
180
+ created_at: datetime
181
+ expires_at: datetime
182
+
183
+ model_config = {"from_attributes": True}
184
+
185
+
186
+ # ─── Alerts ───────────────────────────────────────────────────────────────────
187
+
188
+ class CreateAlertConfigRequest(BaseModel):
189
+ name: str = Field(..., min_length=1, max_length=100)
190
+ alert_type: str = Field(..., pattern="^(offline|error_rate|latency|token_budget)$")
191
+ metric: str
192
+ condition: str = Field(..., pattern="^(gt|lt|gte|lte|eq)$")
193
+ threshold: float
194
+ window_seconds: int = Field(300, ge=60, le=86400)
195
+ cooldown_seconds: int = Field(3600, ge=60, le=604800)
196
+ severity: str = Field("warning", pattern="^(info|warning|critical)$")
197
+ channels: list[str] = ["email"]
198
+
199
+
200
+ class UpdateAlertConfigRequest(BaseModel):
201
+ name: str | None = None
202
+ threshold: float | None = None
203
+ window_seconds: int | None = None
204
+ cooldown_seconds: int | None = None
205
+ severity: str | None = None
206
+ channels: list[str] | None = None
207
+ is_enabled: bool | None = None
208
+
209
+
210
+ class AlertConfigResponse(BaseModel):
211
+ id: str
212
+ name: str
213
+ alert_type: str
214
+ metric: str
215
+ condition: str
216
+ threshold: float
217
+ window_seconds: int
218
+ cooldown_seconds: int
219
+ severity: str
220
+ channels: list[str]
221
+ is_enabled: bool
222
+ last_triggered_at: datetime | None = None
223
+ created_at: datetime
224
+
225
+ model_config = {"from_attributes": True}
226
+
227
+
228
+ class AlertHistoryResponse(BaseModel):
229
+ id: str
230
+ alert_type: str
231
+ severity: str
232
+ status: str
233
+ message: str | None = None
234
+ trigger_value: float | None = None
235
+ threshold_value: float | None = None
236
+ acknowledged_at: datetime | None = None
237
+ resolved_at: datetime | None = None
238
+ created_at: datetime
239
+
240
+ model_config = {"from_attributes": True}
241
+
242
+
243
+ # ─── Setup Token ──────────────────────────────────────────────────────────────
244
+
245
+ class SetupTokenResponse(BaseModel):
246
+ token: str # Only shown once!
247
+ expires_at: datetime
248
+ agent_id: str
249
+
250
+
251
+ class AgentRegistrationRequest(BaseModel):
252
+ setup_token: str
253
+ sdk_version: str
254
+ host_info: HostInfo | None = None
255
+
256
+
257
+ class AgentRegistrationResponse(BaseModel):
258
+ agent_id: str
259
+ name: str
260
+ model_preference: str
261
+ system_prompt: str | None = None
262
+ api_key: str # Agent-scoped API key, shown once
263
+
264
+
265
+ # ─── Telemetry Batch ──────────────────────────────────────────────────────────
266
+
267
+ class TelemetryEvent(BaseModel):
268
+ request_id: str
269
+ timestamp_start: datetime
270
+ timestamp_end: datetime
271
+ latency_ms: int
272
+ input_tokens: int
273
+ output_tokens: int
274
+ model: str
275
+ status: str = Field(..., pattern="^(success|error|timeout)$")
276
+ error_type: str | None = None
277
+ tool_calls: list[dict] | None = None
278
+
279
+
280
+ class TelemetryBatchRequest(BaseModel):
281
+ agent_id: str
282
+ events: list[TelemetryEvent]
283
+ idempotency_key: str | None = None
284
+
285
+
286
+ class TelemetryBatchResponse(BaseModel):
287
+ accepted: int
288
+ rejected: int
289
+ errors: list[str] = []
@@ -16,6 +16,9 @@ class ApiKeyResponse(BaseModel):
16
16
  last_used_at: datetime | None = None
17
17
  created_at: datetime
18
18
  expires_at: datetime | None = None
19
+ key_type: str = "api" # api, agent
20
+ agent_id: str | None = None
21
+ agent_name: str | None = None
19
22
 
20
23
  model_config = {"from_attributes": True}
21
24
 
@@ -0,0 +1,52 @@
1
+ """
2
+ Agent services module.
3
+
4
+ This module provides modular, focused services for agent management:
5
+
6
+ Core Services:
7
+ - AgentService: Main composite service (backward compatible API)
8
+ - AgentCrudService: CRUD operations for agents
9
+ - AgentHeartbeatService: Heartbeat processing and state management
10
+ - AgentCommandsService: Command issuing and acknowledgment
11
+ - AgentSetupService: Setup tokens and API key creation
12
+ - AgentEventsService: Event logging and queries
13
+
14
+ Metrics Services:
15
+ - AgentMetricsService: Metrics queries and telemetry processing
16
+ - AgentMetricsRollupService: Background rollup and cleanup jobs
17
+
18
+ Alerts Services:
19
+ - AgentAlertsService: Alert configuration CRUD
20
+ - AgentAlertsEvaluationService: Background alert evaluation
21
+
22
+ The main AgentService class composes all sub-services and maintains
23
+ backward compatibility with the original monolithic API.
24
+ """
25
+
26
+ from backend.services.agent.agent_service import AgentService
27
+ from backend.services.agent.agent_crud_service import AgentCrudService
28
+ from backend.services.agent.agent_heartbeat_service import AgentHeartbeatService
29
+ from backend.services.agent.agent_commands_service import AgentCommandsService
30
+ from backend.services.agent.agent_metrics_service import AgentMetricsService
31
+ from backend.services.agent.agent_metrics_rollup_service import AgentMetricsRollupService
32
+ from backend.services.agent.agent_alerts_service import AgentAlertsService
33
+ from backend.services.agent.agent_alerts_evaluation_service import AgentAlertsEvaluationService
34
+ from backend.services.agent.agent_setup_service import AgentSetupService
35
+ from backend.services.agent.agent_events_service import AgentEventsService
36
+
37
+ __all__ = [
38
+ # Main composite service
39
+ "AgentService",
40
+ # Core services
41
+ "AgentCrudService",
42
+ "AgentHeartbeatService",
43
+ "AgentCommandsService",
44
+ "AgentSetupService",
45
+ "AgentEventsService",
46
+ # Metrics services
47
+ "AgentMetricsService",
48
+ "AgentMetricsRollupService",
49
+ # Alerts services
50
+ "AgentAlertsService",
51
+ "AgentAlertsEvaluationService",
52
+ ]