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.
- kairo/backend/api/agents.py +337 -16
- kairo/backend/app.py +84 -4
- kairo/backend/config.py +4 -2
- kairo/backend/models/agent.py +216 -2
- kairo/backend/models/api_key.py +4 -1
- kairo/backend/models/task.py +31 -0
- kairo/backend/models/user_provider_key.py +26 -0
- kairo/backend/schemas/agent.py +249 -2
- kairo/backend/schemas/api_key.py +3 -0
- kairo/backend/services/agent/__init__.py +52 -0
- kairo/backend/services/agent/agent_alerts_evaluation_service.py +224 -0
- kairo/backend/services/agent/agent_alerts_service.py +201 -0
- kairo/backend/services/agent/agent_commands_service.py +142 -0
- kairo/backend/services/agent/agent_crud_service.py +150 -0
- kairo/backend/services/agent/agent_events_service.py +103 -0
- kairo/backend/services/agent/agent_heartbeat_service.py +207 -0
- kairo/backend/services/agent/agent_metrics_rollup_service.py +248 -0
- kairo/backend/services/agent/agent_metrics_service.py +259 -0
- kairo/backend/services/agent/agent_service.py +315 -0
- kairo/backend/services/agent/agent_setup_service.py +180 -0
- kairo/backend/services/agent/constants.py +28 -0
- kairo/backend/services/agent_service.py +18 -102
- kairo/backend/services/api_key_service.py +23 -3
- kairo/backend/services/byok_service.py +204 -0
- kairo/backend/services/chat_service.py +398 -63
- kairo/backend/services/deep_search_service.py +159 -0
- kairo/backend/services/email_service.py +418 -19
- kairo/backend/services/few_shot_service.py +223 -0
- kairo/backend/services/post_processor.py +261 -0
- kairo/backend/services/rag_service.py +150 -0
- kairo/backend/services/task_service.py +119 -0
- kairo/backend/tests/__init__.py +1 -0
- kairo/backend/tests/e2e/__init__.py +1 -0
- kairo/backend/tests/e2e/agents/__init__.py +1 -0
- kairo/backend/tests/e2e/agents/conftest.py +389 -0
- kairo/backend/tests/e2e/agents/test_agent_alerts.py +802 -0
- kairo/backend/tests/e2e/agents/test_agent_commands.py +456 -0
- kairo/backend/tests/e2e/agents/test_agent_crud.py +455 -0
- kairo/backend/tests/e2e/agents/test_agent_events.py +415 -0
- kairo/backend/tests/e2e/agents/test_agent_heartbeat.py +520 -0
- kairo/backend/tests/e2e/agents/test_agent_metrics.py +587 -0
- kairo/backend/tests/e2e/agents/test_agent_setup.py +349 -0
- kairo/migrations/versions/010_agent_dashboard.py +246 -0
- {kairo_code-0.1.0.dist-info → kairo_code-0.2.0.dist-info}/METADATA +1 -1
- {kairo_code-0.1.0.dist-info → kairo_code-0.2.0.dist-info}/RECORD +50 -16
- {kairo_code-0.1.0.dist-info → kairo_code-0.2.0.dist-info}/top_level.txt +1 -0
- kairo_migrations/env.py +92 -0
- kairo_migrations/versions/001_add_agent_dashboard_extensions.py +450 -0
- {kairo_code-0.1.0.dist-info → kairo_code-0.2.0.dist-info}/WHEEL +0 -0
- {kairo_code-0.1.0.dist-info → kairo_code-0.2.0.dist-info}/entry_points.txt +0 -0
kairo/backend/models/agent.py
CHANGED
|
@@ -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
|
-
|
|
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
|
+
)
|
kairo/backend/models/api_key.py
CHANGED
|
@@ -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
|
+
)
|
kairo/backend/schemas/agent.py
CHANGED
|
@@ -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
|
-
|
|
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] = []
|
kairo/backend/schemas/api_key.py
CHANGED
|
@@ -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
|
+
]
|