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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import datetime, UTC
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import select, text
|
|
5
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
6
|
+
|
|
7
|
+
from backend.models.agent import Agent
|
|
8
|
+
from backend.models.task import Task
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TaskService:
|
|
14
|
+
def __init__(self, db: AsyncSession):
|
|
15
|
+
self.db = db
|
|
16
|
+
|
|
17
|
+
async def create_task(self, user_id: str, input_data: dict, agent_id: str | None = None) -> Task:
|
|
18
|
+
"""Create a new task for a user, optionally targeting a specific agent."""
|
|
19
|
+
# If agent_id specified, verify the agent belongs to this user
|
|
20
|
+
if agent_id:
|
|
21
|
+
stmt = select(Agent).where(Agent.id == agent_id, Agent.user_id == user_id)
|
|
22
|
+
result = await self.db.execute(stmt)
|
|
23
|
+
if not result.scalar_one_or_none():
|
|
24
|
+
raise ValueError("Agent not found or not owned by user")
|
|
25
|
+
|
|
26
|
+
task = Task(
|
|
27
|
+
user_id=user_id,
|
|
28
|
+
agent_id=agent_id,
|
|
29
|
+
input=input_data,
|
|
30
|
+
status="pending",
|
|
31
|
+
)
|
|
32
|
+
self.db.add(task)
|
|
33
|
+
await self.db.commit()
|
|
34
|
+
await self.db.refresh(task)
|
|
35
|
+
logger.info("Task created: id=%s user=%s agent=%s", task.id, user_id, agent_id)
|
|
36
|
+
return task
|
|
37
|
+
|
|
38
|
+
async def claim_task(self, agent_id: str, user_id: str) -> Task | None:
|
|
39
|
+
"""Claim the oldest pending task for this user's agent.
|
|
40
|
+
|
|
41
|
+
Uses FOR UPDATE SKIP LOCKED to prevent race conditions
|
|
42
|
+
when multiple agents poll simultaneously.
|
|
43
|
+
"""
|
|
44
|
+
# Verify agent belongs to this user
|
|
45
|
+
agent_stmt = select(Agent).where(Agent.id == agent_id, Agent.user_id == user_id)
|
|
46
|
+
agent_result = await self.db.execute(agent_stmt)
|
|
47
|
+
agent = agent_result.scalar_one_or_none()
|
|
48
|
+
if not agent:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
# Find oldest pending task: either targeted to this agent or unassigned
|
|
52
|
+
stmt = (
|
|
53
|
+
select(Task)
|
|
54
|
+
.where(
|
|
55
|
+
Task.user_id == user_id,
|
|
56
|
+
Task.status == "pending",
|
|
57
|
+
(Task.agent_id == agent_id) | (Task.agent_id.is_(None)),
|
|
58
|
+
)
|
|
59
|
+
.order_by(Task.created_at.asc())
|
|
60
|
+
.limit(1)
|
|
61
|
+
.with_for_update(skip_locked=True)
|
|
62
|
+
)
|
|
63
|
+
result = await self.db.execute(stmt)
|
|
64
|
+
task = result.scalar_one_or_none()
|
|
65
|
+
|
|
66
|
+
if not task:
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
task.agent_id = agent_id
|
|
70
|
+
task.status = "running"
|
|
71
|
+
task.claimed_at = datetime.now(UTC)
|
|
72
|
+
task.updated_at = datetime.now(UTC)
|
|
73
|
+
await self.db.commit()
|
|
74
|
+
await self.db.refresh(task)
|
|
75
|
+
logger.info("Task claimed: id=%s agent=%s", task.id, agent_id)
|
|
76
|
+
return task
|
|
77
|
+
|
|
78
|
+
async def update_task(
|
|
79
|
+
self, task_id: str, user_id: str, status: str,
|
|
80
|
+
output: dict | None = None, error: str | None = None,
|
|
81
|
+
) -> Task | None:
|
|
82
|
+
"""Update a task's status, output, or error."""
|
|
83
|
+
stmt = select(Task).where(Task.id == task_id, Task.user_id == user_id)
|
|
84
|
+
result = await self.db.execute(stmt)
|
|
85
|
+
task = result.scalar_one_or_none()
|
|
86
|
+
if not task:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
task.status = status
|
|
90
|
+
task.updated_at = datetime.now(UTC)
|
|
91
|
+
|
|
92
|
+
if output is not None:
|
|
93
|
+
task.output = output
|
|
94
|
+
if error is not None:
|
|
95
|
+
task.error = error
|
|
96
|
+
if status in ("completed", "failed"):
|
|
97
|
+
task.completed_at = datetime.now(UTC)
|
|
98
|
+
|
|
99
|
+
await self.db.commit()
|
|
100
|
+
await self.db.refresh(task)
|
|
101
|
+
logger.info("Task updated: id=%s status=%s", task_id, status)
|
|
102
|
+
return task
|
|
103
|
+
|
|
104
|
+
async def list_tasks(
|
|
105
|
+
self, user_id: str, status: str | None = None, limit: int = 50,
|
|
106
|
+
) -> list[Task]:
|
|
107
|
+
"""List tasks for a user, optionally filtered by status."""
|
|
108
|
+
stmt = select(Task).where(Task.user_id == user_id)
|
|
109
|
+
if status:
|
|
110
|
+
stmt = stmt.where(Task.status == status)
|
|
111
|
+
stmt = stmt.order_by(Task.created_at.desc()).limit(limit)
|
|
112
|
+
result = await self.db.execute(stmt)
|
|
113
|
+
return list(result.scalars().all())
|
|
114
|
+
|
|
115
|
+
async def get_task(self, task_id: str, user_id: str) -> Task | None:
|
|
116
|
+
"""Get a single task by ID, scoped to user."""
|
|
117
|
+
stmt = select(Task).where(Task.id == task_id, Task.user_id == user_id)
|
|
118
|
+
result = await self.db.execute(stmt)
|
|
119
|
+
return result.scalar_one_or_none()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Tests package
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# End-to-end tests package
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Agent end-to-end tests package
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared fixtures for agent end-to-end tests.
|
|
3
|
+
|
|
4
|
+
Provides test database setup, authenticated clients, and factory functions
|
|
5
|
+
for creating test data.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import hashlib
|
|
10
|
+
import secrets
|
|
11
|
+
import uuid
|
|
12
|
+
from datetime import datetime, timedelta, UTC
|
|
13
|
+
from typing import AsyncGenerator, Generator
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
import pytest_asyncio
|
|
17
|
+
from argon2 import PasswordHasher
|
|
18
|
+
from httpx import ASGITransport, AsyncClient
|
|
19
|
+
from sqlalchemy import text
|
|
20
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
|
21
|
+
|
|
22
|
+
from backend.app import create_app
|
|
23
|
+
from backend.config import settings
|
|
24
|
+
from backend.core.database import Base, get_db
|
|
25
|
+
from backend.models.agent import (
|
|
26
|
+
Agent,
|
|
27
|
+
AgentAlertConfig,
|
|
28
|
+
AgentAlertHistory,
|
|
29
|
+
AgentCommand,
|
|
30
|
+
AgentEvent,
|
|
31
|
+
AgentMetrics1m,
|
|
32
|
+
AgentSetupToken,
|
|
33
|
+
)
|
|
34
|
+
from backend.models.api_key import ApiKey
|
|
35
|
+
from backend.models.user import User
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Use a test database URL (modify for actual test environment)
|
|
39
|
+
TEST_DATABASE_URL = settings.DATABASE_URL.replace("/kairo", "/kairo_test")
|
|
40
|
+
|
|
41
|
+
# Create test engine and session
|
|
42
|
+
test_engine = create_async_engine(TEST_DATABASE_URL, echo=False)
|
|
43
|
+
TestAsyncSession = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.fixture(scope="session")
|
|
47
|
+
def event_loop() -> Generator:
|
|
48
|
+
"""Create an instance of the default event loop for the test session."""
|
|
49
|
+
loop = asyncio.get_event_loop_policy().new_event_loop()
|
|
50
|
+
yield loop
|
|
51
|
+
loop.close()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest_asyncio.fixture(scope="function")
|
|
55
|
+
async def db_session() -> AsyncGenerator[AsyncSession, None]:
|
|
56
|
+
"""Create a fresh database session for each test."""
|
|
57
|
+
async with test_engine.begin() as conn:
|
|
58
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
59
|
+
|
|
60
|
+
async with TestAsyncSession() as session:
|
|
61
|
+
yield session
|
|
62
|
+
# Rollback any uncommitted changes
|
|
63
|
+
await session.rollback()
|
|
64
|
+
|
|
65
|
+
# Clean up tables after test
|
|
66
|
+
async with test_engine.begin() as conn:
|
|
67
|
+
await conn.run_sync(Base.metadata.drop_all)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@pytest_asyncio.fixture
|
|
71
|
+
async def app(db_session: AsyncSession):
|
|
72
|
+
"""Create a test application with dependency overrides."""
|
|
73
|
+
application = create_app()
|
|
74
|
+
|
|
75
|
+
# Override the database dependency
|
|
76
|
+
async def override_get_db():
|
|
77
|
+
yield db_session
|
|
78
|
+
|
|
79
|
+
application.dependency_overrides[get_db] = override_get_db
|
|
80
|
+
|
|
81
|
+
# Enable agents feature for testing
|
|
82
|
+
original_setting = settings.FEATURE_KAIRO_AGENTS_ENABLED
|
|
83
|
+
settings.FEATURE_KAIRO_AGENTS_ENABLED = True
|
|
84
|
+
|
|
85
|
+
yield application
|
|
86
|
+
|
|
87
|
+
# Restore original setting
|
|
88
|
+
settings.FEATURE_KAIRO_AGENTS_ENABLED = original_setting
|
|
89
|
+
application.dependency_overrides.clear()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@pytest_asyncio.fixture
|
|
93
|
+
async def client(app) -> AsyncGenerator[AsyncClient, None]:
|
|
94
|
+
"""Create an async HTTP client for testing."""
|
|
95
|
+
transport = ASGITransport(app=app)
|
|
96
|
+
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
|
97
|
+
yield ac
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@pytest_asyncio.fixture
|
|
101
|
+
async def test_user(db_session: AsyncSession) -> User:
|
|
102
|
+
"""Create a test user."""
|
|
103
|
+
ph = PasswordHasher()
|
|
104
|
+
user = User(
|
|
105
|
+
id=str(uuid.uuid4()),
|
|
106
|
+
email=f"test_{uuid.uuid4().hex[:8]}@example.com",
|
|
107
|
+
hashed_password=ph.hash("testpassword123"),
|
|
108
|
+
email_verified=True,
|
|
109
|
+
status="active",
|
|
110
|
+
plan="pro",
|
|
111
|
+
)
|
|
112
|
+
db_session.add(user)
|
|
113
|
+
await db_session.commit()
|
|
114
|
+
await db_session.refresh(user)
|
|
115
|
+
return user
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@pytest_asyncio.fixture
|
|
119
|
+
async def auth_token(test_user: User) -> str:
|
|
120
|
+
"""Generate a valid JWT token for the test user."""
|
|
121
|
+
from backend.core.security import create_access_token
|
|
122
|
+
return create_access_token(test_user.id)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@pytest_asyncio.fixture
|
|
126
|
+
async def auth_headers(auth_token: str) -> dict:
|
|
127
|
+
"""Return authorization headers for authenticated requests."""
|
|
128
|
+
return {"Authorization": f"Bearer {auth_token}"}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@pytest_asyncio.fixture
|
|
132
|
+
async def test_agent(db_session: AsyncSession, test_user: User) -> Agent:
|
|
133
|
+
"""Create a test agent."""
|
|
134
|
+
agent = Agent(
|
|
135
|
+
id=str(uuid.uuid4()),
|
|
136
|
+
user_id=test_user.id,
|
|
137
|
+
name="Test Agent",
|
|
138
|
+
description="A test agent for e2e tests",
|
|
139
|
+
model_preference="nyx",
|
|
140
|
+
agent_type="sdk",
|
|
141
|
+
state="created",
|
|
142
|
+
status="offline",
|
|
143
|
+
)
|
|
144
|
+
db_session.add(agent)
|
|
145
|
+
await db_session.commit()
|
|
146
|
+
await db_session.refresh(agent)
|
|
147
|
+
return agent
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@pytest_asyncio.fixture
|
|
151
|
+
async def online_agent(db_session: AsyncSession, test_user: User) -> Agent:
|
|
152
|
+
"""Create an online test agent with connection info."""
|
|
153
|
+
now = datetime.now(UTC)
|
|
154
|
+
agent = Agent(
|
|
155
|
+
id=str(uuid.uuid4()),
|
|
156
|
+
user_id=test_user.id,
|
|
157
|
+
name="Online Test Agent",
|
|
158
|
+
description="An online agent for testing",
|
|
159
|
+
model_preference="nyx",
|
|
160
|
+
agent_type="sdk",
|
|
161
|
+
state="online",
|
|
162
|
+
status="online",
|
|
163
|
+
sdk_version="1.0.0",
|
|
164
|
+
host_info={"hostname": "test-host", "os": "linux"},
|
|
165
|
+
first_connected_at=now - timedelta(hours=1),
|
|
166
|
+
last_online_at=now,
|
|
167
|
+
last_heartbeat_at=now,
|
|
168
|
+
)
|
|
169
|
+
db_session.add(agent)
|
|
170
|
+
await db_session.commit()
|
|
171
|
+
await db_session.refresh(agent)
|
|
172
|
+
return agent
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@pytest_asyncio.fixture
|
|
176
|
+
async def test_api_key(db_session: AsyncSession, test_user: User, test_agent: Agent) -> tuple[str, ApiKey]:
|
|
177
|
+
"""Create an API key for agent authentication."""
|
|
178
|
+
key_raw = secrets.token_urlsafe(32)
|
|
179
|
+
key = f"sk-kairo-{key_raw}"
|
|
180
|
+
key_prefix = key[:20]
|
|
181
|
+
key_hash = hashlib.sha256(key.encode()).hexdigest()
|
|
182
|
+
|
|
183
|
+
api_key = ApiKey(
|
|
184
|
+
id=str(uuid.uuid4()),
|
|
185
|
+
user_id=test_user.id,
|
|
186
|
+
agent_id=test_agent.id,
|
|
187
|
+
name=f"Agent: {test_agent.name}",
|
|
188
|
+
key_prefix=key_prefix,
|
|
189
|
+
key_hash=key_hash,
|
|
190
|
+
key_type="agent",
|
|
191
|
+
is_active=True,
|
|
192
|
+
)
|
|
193
|
+
db_session.add(api_key)
|
|
194
|
+
await db_session.commit()
|
|
195
|
+
await db_session.refresh(api_key)
|
|
196
|
+
return key, api_key
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@pytest_asyncio.fixture
|
|
200
|
+
async def api_key_headers(test_api_key: tuple[str, ApiKey]) -> dict:
|
|
201
|
+
"""Return authorization headers for API key authentication."""
|
|
202
|
+
key, _ = test_api_key
|
|
203
|
+
return {"Authorization": f"Bearer {key}"}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class AgentFactory:
|
|
207
|
+
"""Factory for creating agent test data."""
|
|
208
|
+
|
|
209
|
+
def __init__(self, db_session: AsyncSession, user_id: str):
|
|
210
|
+
self.db = db_session
|
|
211
|
+
self.user_id = user_id
|
|
212
|
+
|
|
213
|
+
async def create_agent(
|
|
214
|
+
self,
|
|
215
|
+
name: str = "Test Agent",
|
|
216
|
+
state: str = "created",
|
|
217
|
+
**kwargs,
|
|
218
|
+
) -> Agent:
|
|
219
|
+
"""Create an agent with specified attributes."""
|
|
220
|
+
agent = Agent(
|
|
221
|
+
id=str(uuid.uuid4()),
|
|
222
|
+
user_id=self.user_id,
|
|
223
|
+
name=name,
|
|
224
|
+
state=state,
|
|
225
|
+
status=kwargs.get("status", "offline"),
|
|
226
|
+
model_preference=kwargs.get("model_preference", "nyx"),
|
|
227
|
+
agent_type=kwargs.get("agent_type", "sdk"),
|
|
228
|
+
description=kwargs.get("description"),
|
|
229
|
+
system_prompt=kwargs.get("system_prompt"),
|
|
230
|
+
sdk_version=kwargs.get("sdk_version"),
|
|
231
|
+
host_info=kwargs.get("host_info"),
|
|
232
|
+
last_heartbeat_at=kwargs.get("last_heartbeat_at"),
|
|
233
|
+
last_online_at=kwargs.get("last_online_at"),
|
|
234
|
+
first_connected_at=kwargs.get("first_connected_at"),
|
|
235
|
+
)
|
|
236
|
+
self.db.add(agent)
|
|
237
|
+
await self.db.commit()
|
|
238
|
+
await self.db.refresh(agent)
|
|
239
|
+
return agent
|
|
240
|
+
|
|
241
|
+
async def create_setup_token(
|
|
242
|
+
self,
|
|
243
|
+
agent_id: str,
|
|
244
|
+
ttl_minutes: int = 15,
|
|
245
|
+
used: bool = False,
|
|
246
|
+
) -> tuple[str, AgentSetupToken]:
|
|
247
|
+
"""Create a setup token for an agent."""
|
|
248
|
+
token = f"kairo_setup_{secrets.token_urlsafe(32)}"
|
|
249
|
+
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
|
250
|
+
expires_at = datetime.now(UTC) + timedelta(minutes=ttl_minutes)
|
|
251
|
+
|
|
252
|
+
setup_token = AgentSetupToken(
|
|
253
|
+
id=str(uuid.uuid4()),
|
|
254
|
+
agent_id=agent_id,
|
|
255
|
+
user_id=self.user_id,
|
|
256
|
+
token_hash=token_hash,
|
|
257
|
+
expires_at=expires_at,
|
|
258
|
+
used=used,
|
|
259
|
+
)
|
|
260
|
+
self.db.add(setup_token)
|
|
261
|
+
await self.db.commit()
|
|
262
|
+
await self.db.refresh(setup_token)
|
|
263
|
+
return token, setup_token
|
|
264
|
+
|
|
265
|
+
async def create_command(
|
|
266
|
+
self,
|
|
267
|
+
agent_id: str,
|
|
268
|
+
command_type: str = "restart",
|
|
269
|
+
status: str = "pending",
|
|
270
|
+
**kwargs,
|
|
271
|
+
) -> AgentCommand:
|
|
272
|
+
"""Create a command for an agent."""
|
|
273
|
+
now = datetime.now(UTC)
|
|
274
|
+
command = AgentCommand(
|
|
275
|
+
id=str(uuid.uuid4()),
|
|
276
|
+
agent_id=agent_id,
|
|
277
|
+
command_type=command_type,
|
|
278
|
+
status=status,
|
|
279
|
+
issued_by=kwargs.get("issued_by", self.user_id),
|
|
280
|
+
payload=kwargs.get("payload"),
|
|
281
|
+
expires_at=kwargs.get("expires_at", now + timedelta(minutes=5)),
|
|
282
|
+
)
|
|
283
|
+
self.db.add(command)
|
|
284
|
+
await self.db.commit()
|
|
285
|
+
await self.db.refresh(command)
|
|
286
|
+
return command
|
|
287
|
+
|
|
288
|
+
async def create_event(
|
|
289
|
+
self,
|
|
290
|
+
agent_id: str,
|
|
291
|
+
event_type: str = "heartbeat",
|
|
292
|
+
**kwargs,
|
|
293
|
+
) -> AgentEvent:
|
|
294
|
+
"""Create an event for an agent."""
|
|
295
|
+
event = AgentEvent(
|
|
296
|
+
id=str(uuid.uuid4()),
|
|
297
|
+
agent_id=agent_id,
|
|
298
|
+
event_type=event_type,
|
|
299
|
+
event_data=kwargs.get("event_data"),
|
|
300
|
+
error_type=kwargs.get("error_type"),
|
|
301
|
+
error_message=kwargs.get("error_message"),
|
|
302
|
+
client_ip=kwargs.get("client_ip"),
|
|
303
|
+
)
|
|
304
|
+
self.db.add(event)
|
|
305
|
+
await self.db.commit()
|
|
306
|
+
await self.db.refresh(event)
|
|
307
|
+
return event
|
|
308
|
+
|
|
309
|
+
async def create_metrics(
|
|
310
|
+
self,
|
|
311
|
+
agent_id: str,
|
|
312
|
+
bucket_time: datetime | None = None,
|
|
313
|
+
**kwargs,
|
|
314
|
+
) -> AgentMetrics1m:
|
|
315
|
+
"""Create metrics for an agent."""
|
|
316
|
+
if bucket_time is None:
|
|
317
|
+
bucket_time = datetime.now(UTC).replace(second=0, microsecond=0)
|
|
318
|
+
|
|
319
|
+
metrics = AgentMetrics1m(
|
|
320
|
+
id=str(uuid.uuid4()),
|
|
321
|
+
agent_id=agent_id,
|
|
322
|
+
bucket_time=bucket_time,
|
|
323
|
+
request_count=kwargs.get("request_count", 10),
|
|
324
|
+
error_count=kwargs.get("error_count", 1),
|
|
325
|
+
input_tokens=kwargs.get("input_tokens", 1000),
|
|
326
|
+
output_tokens=kwargs.get("output_tokens", 500),
|
|
327
|
+
total_latency_ms=kwargs.get("total_latency_ms", 5000),
|
|
328
|
+
tool_calls=kwargs.get("tool_calls", 2),
|
|
329
|
+
)
|
|
330
|
+
self.db.add(metrics)
|
|
331
|
+
await self.db.commit()
|
|
332
|
+
await self.db.refresh(metrics)
|
|
333
|
+
return metrics
|
|
334
|
+
|
|
335
|
+
async def create_alert_config(
|
|
336
|
+
self,
|
|
337
|
+
agent_id: str,
|
|
338
|
+
name: str = "Test Alert",
|
|
339
|
+
**kwargs,
|
|
340
|
+
) -> AgentAlertConfig:
|
|
341
|
+
"""Create an alert configuration for an agent."""
|
|
342
|
+
config = AgentAlertConfig(
|
|
343
|
+
id=str(uuid.uuid4()),
|
|
344
|
+
agent_id=agent_id,
|
|
345
|
+
user_id=self.user_id,
|
|
346
|
+
name=name,
|
|
347
|
+
alert_type=kwargs.get("alert_type", "error_rate"),
|
|
348
|
+
metric=kwargs.get("metric", "error_rate"),
|
|
349
|
+
condition=kwargs.get("condition", "gt"),
|
|
350
|
+
threshold=kwargs.get("threshold", 10.0),
|
|
351
|
+
window_seconds=kwargs.get("window_seconds", 300),
|
|
352
|
+
cooldown_seconds=kwargs.get("cooldown_seconds", 3600),
|
|
353
|
+
severity=kwargs.get("severity", "warning"),
|
|
354
|
+
channels=kwargs.get("channels", ["email"]),
|
|
355
|
+
is_enabled=kwargs.get("is_enabled", True),
|
|
356
|
+
)
|
|
357
|
+
self.db.add(config)
|
|
358
|
+
await self.db.commit()
|
|
359
|
+
await self.db.refresh(config)
|
|
360
|
+
return config
|
|
361
|
+
|
|
362
|
+
async def create_alert_history(
|
|
363
|
+
self,
|
|
364
|
+
config_id: str,
|
|
365
|
+
agent_id: str,
|
|
366
|
+
**kwargs,
|
|
367
|
+
) -> AgentAlertHistory:
|
|
368
|
+
"""Create an alert history entry."""
|
|
369
|
+
history = AgentAlertHistory(
|
|
370
|
+
id=str(uuid.uuid4()),
|
|
371
|
+
config_id=config_id,
|
|
372
|
+
agent_id=agent_id,
|
|
373
|
+
alert_type=kwargs.get("alert_type", "error_rate"),
|
|
374
|
+
severity=kwargs.get("severity", "warning"),
|
|
375
|
+
status=kwargs.get("status", "triggered"),
|
|
376
|
+
message=kwargs.get("message", "Error rate exceeded threshold"),
|
|
377
|
+
trigger_value=kwargs.get("trigger_value", 15.0),
|
|
378
|
+
threshold_value=kwargs.get("threshold_value", 10.0),
|
|
379
|
+
)
|
|
380
|
+
self.db.add(history)
|
|
381
|
+
await self.db.commit()
|
|
382
|
+
await self.db.refresh(history)
|
|
383
|
+
return history
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@pytest_asyncio.fixture
|
|
387
|
+
async def agent_factory(db_session: AsyncSession, test_user: User) -> AgentFactory:
|
|
388
|
+
"""Provide an agent factory for creating test data."""
|
|
389
|
+
return AgentFactory(db_session, test_user.id)
|