1use crate::db::tenant_agents::{self, TenantAgent};
8use crate::db::agent_runs;
9use crate::models::TenantContext;
10use crate::types::{AppError, Result};
11use crate::AppState;
12use axum::{
13 extract::{Extension, Path, Query, State},
14 http::StatusCode,
15 Json,
16};
17use chrono::{DateTime, Datelike, TimeZone, Utc};
18use serde::{Deserialize, Serialize};
19
20#[derive(Debug, Serialize)]
25pub struct V1Agent {
26 pub id: String,
27 pub name: String,
28 pub agent_type: String,
29 pub status: V1AgentStatus,
30 pub config: serde_json::Value,
31 pub created_at: DateTime<Utc>,
32 pub last_run: Option<DateTime<Utc>>,
33 pub total_runs: u64,
34 pub success_rate: f64,
35}
36
37#[derive(Debug, Serialize)]
38#[serde(rename_all = "snake_case")]
39pub enum V1AgentStatus {
40 Active,
41 Idle,
42 Error,
43 Disabled,
44}
45
46impl From<TenantAgent> for V1Agent {
47 fn from(a: TenantAgent) -> Self {
48 let status = if a.enabled {
49 V1AgentStatus::Active
50 } else {
51 V1AgentStatus::Disabled
52 };
53 Self {
54 id: a.id,
55 name: a.agent_name,
56 agent_type: "custom".to_string(),
57 status,
58 config: a.config,
59 created_at: ts_to_dt(a.created_at),
60 last_run: None,
61 total_runs: 0,
62 success_rate: 0.0,
63 }
64 }
65}
66
67#[derive(Debug, Serialize)]
68pub struct V1AgentRun {
69 pub id: String,
70 pub agent_id: String,
71 pub status: String,
72 pub input: serde_json::Value,
73 pub output: Option<serde_json::Value>,
74 pub error: Option<String>,
75 pub started_at: DateTime<Utc>,
76 pub finished_at: Option<DateTime<Utc>>,
77 pub duration_ms: Option<u64>,
78 pub tokens_used: Option<u64>,
79}
80
81#[derive(Debug, Serialize)]
82pub struct V1AgentLog {
83 pub id: String,
84 pub agent_id: String,
85 pub run_id: Option<String>,
86 pub level: String,
87 pub message: String,
88 pub metadata: Option<serde_json::Value>,
89 pub timestamp: DateTime<Utc>,
90}
91
92#[derive(Debug, Serialize)]
93pub struct Paginated<T> {
94 pub items: Vec<T>,
95 pub total: u64,
96 pub page: u32,
97 pub per_page: u32,
98 pub total_pages: u32,
99}
100
101impl<T> Paginated<T> {
102 fn empty(page: u32, per_page: u32) -> Self {
103 Self {
104 items: vec![],
105 total: 0,
106 page,
107 per_page,
108 total_pages: 0,
109 }
110 }
111}
112
113#[derive(Debug, Serialize)]
114pub struct V1Usage {
115 pub period_start: DateTime<Utc>,
116 pub period_end: DateTime<Utc>,
117 pub total_runs: u64,
118 pub total_tokens: u64,
119 pub total_api_calls: u64,
120 pub quota_runs: Option<u64>,
121 pub quota_tokens: Option<u64>,
122 pub daily_usage: Vec<DailyUsage>,
123}
124
125#[derive(Debug, Serialize)]
126pub struct DailyUsage {
127 pub date: String,
128 pub runs: u64,
129 pub tokens: u64,
130 pub api_calls: u64,
131}
132
133#[derive(Debug, Serialize)]
134pub struct V1ApiKey {
135 pub id: String,
136 pub name: String,
137 pub prefix: String,
138 pub created_at: DateTime<Utc>,
139 pub last_used: Option<DateTime<Utc>>,
140 pub expires_at: Option<DateTime<Utc>>,
141}
142
143#[derive(Debug, Deserialize)]
144pub struct CreateApiKeyRequest {
145 pub name: String,
146 pub expires_in_days: Option<u32>,
147}
148
149#[derive(Debug, Serialize)]
150pub struct CreateApiKeyResponse {
151 pub key: V1ApiKey,
152 pub secret: String,
153}
154
155#[derive(Debug, Deserialize)]
156pub struct PaginationQuery {
157 pub page: Option<u32>,
158 pub per_page: Option<u32>,
159}
160
161fn ts_to_dt(ts: i64) -> DateTime<Utc> {
166 Utc.timestamp_opt(ts, 0).single().unwrap_or_else(Utc::now)
167}
168
169fn extract_tenant(ctx: Option<Extension<TenantContext>>) -> Result<TenantContext> {
170 ctx.map(|Extension(c)| c)
171 .ok_or_else(|| AppError::Auth("Missing tenant context".to_string()))
172}
173
174pub async fn list_agents(
180 State(state): State<AppState>,
181 ctx: Option<Extension<TenantContext>>,
182 Query(q): Query<PaginationQuery>,
183) -> Result<Json<Paginated<V1Agent>>> {
184 let tc = extract_tenant(ctx)?;
185 let page = q.page.unwrap_or(1).max(1);
186 let per_page = q.per_page.unwrap_or(20).min(100);
187
188 let agents = tenant_agents::list_tenant_agents(state.tenant_db.pool(), &tc.tenant_id).await?;
189 let total = agents.len() as u64;
190 let total_pages = ((total as f64) / (per_page as f64)).ceil() as u32;
191
192 let start = ((page - 1) * per_page) as usize;
193 let items: Vec<V1Agent> = agents
194 .into_iter()
195 .skip(start)
196 .take(per_page as usize)
197 .map(V1Agent::from)
198 .collect();
199
200 Ok(Json(Paginated {
201 items,
202 total,
203 page,
204 per_page,
205 total_pages,
206 }))
207}
208
209pub async fn get_agent(
211 State(state): State<AppState>,
212 ctx: Option<Extension<TenantContext>>,
213 Path(name): Path<String>,
214) -> Result<Json<V1Agent>> {
215 let tc = extract_tenant(ctx)?;
216 let agent = tenant_agents::get_tenant_agent(state.tenant_db.pool(), &tc.tenant_id, &name).await?;
217 Ok(Json(V1Agent::from(agent)))
218}
219
220pub async fn run_agent(
222 State(state): State<AppState>,
223 ctx: Option<Extension<TenantContext>>,
224 Path(name): Path<String>,
225 Json(input): Json<serde_json::Value>,
226) -> Result<Json<V1AgentRun>> {
227 let tc = extract_tenant(ctx)?;
228 let _agent = tenant_agents::get_tenant_agent(state.tenant_db.pool(), &tc.tenant_id, &name).await?;
230
231 Ok(Json(V1AgentRun {
233 id: uuid::Uuid::new_v4().to_string(),
234 agent_id: name,
235 status: "completed".to_string(),
236 input,
237 output: Some(serde_json::json!({"message": "Agent run queued"})),
238 error: None,
239 started_at: Utc::now(),
240 finished_at: Some(Utc::now()),
241 duration_ms: Some(0),
242 tokens_used: Some(0),
243 }))
244}
245
246pub async fn list_agent_runs(
248 State(state): State<AppState>,
249 ctx: Option<Extension<TenantContext>>,
250 Path(name): Path<String>,
251 Query(q): Query<PaginationQuery>,
252) -> Result<Json<Paginated<V1AgentRun>>> {
253 let tc = extract_tenant(ctx)?;
254 let page = q.page.unwrap_or(1).max(1);
255 let per_page = q.per_page.unwrap_or(25).min(100);
256 let offset = ((page - 1) * per_page) as i64;
257
258 let runs = agent_runs::list_agent_runs(
259 state.tenant_db.pool(),
260 &tc.tenant_id,
261 Some(&name),
262 per_page as i64,
263 offset,
264 ).await?;
265
266 let items: Vec<V1AgentRun> = runs.into_iter().map(|r| V1AgentRun {
267 id: r.id,
268 agent_id: r.agent_name,
269 status: r.status,
270 input: serde_json::json!({"tokens": r.input_tokens}),
271 output: Some(serde_json::json!({"tokens": r.output_tokens})),
272 error: r.error,
273 started_at: ts_to_dt(r.created_at),
274 finished_at: Some(ts_to_dt(r.created_at + (r.duration_ms / 1000))),
275 duration_ms: Some(r.duration_ms as u64),
276 tokens_used: Some((r.input_tokens + r.output_tokens) as u64),
277 }).collect();
278
279 let total = items.len() as u64;
280 Ok(Json(Paginated {
281 items,
282 total,
283 page,
284 per_page,
285 total_pages: ((total as f64) / (per_page as f64)).ceil() as u32,
286 }))
287}
288
289pub async fn list_agent_logs(
291 ctx: Option<Extension<TenantContext>>,
292 Path(name): Path<String>,
293 Query(q): Query<PaginationQuery>,
294) -> Result<Json<Paginated<V1AgentLog>>> {
295 let _tc = extract_tenant(ctx)?;
296 let page = q.page.unwrap_or(1);
297 let per_page = q.per_page.unwrap_or(50);
298 let _ = name;
299 Ok(Json(Paginated::empty(page, per_page)))
300}
301
302pub async fn get_usage(
304 State(state): State<AppState>,
305 ctx: Option<Extension<TenantContext>>,
306) -> Result<Json<V1Usage>> {
307 let tc = extract_tenant(ctx)?;
308 let summary = state.tenant_db.get_usage_summary(&tc.tenant_id).await?;
309
310 let now = Utc::now();
311 let period_start = now
312 .date_naive()
313 .with_day(1)
314 .unwrap()
315 .and_hms_opt(0, 0, 0)
316 .unwrap()
317 .and_utc();
318
319 let quota_runs = if tc.quota.requests_per_month == u64::MAX {
321 None
322 } else {
323 Some(tc.quota.requests_per_month)
324 };
325 let quota_tokens = if tc.quota.tokens_per_month == u64::MAX {
326 None
327 } else {
328 Some(tc.quota.tokens_per_month)
329 };
330
331 Ok(Json(V1Usage {
332 period_start,
333 period_end: now,
334 total_runs: summary.monthly_requests,
335 total_tokens: summary.monthly_tokens,
336 total_api_calls: summary.monthly_requests,
337 quota_runs,
338 quota_tokens,
339 daily_usage: vec![],
340 }))
341}
342
343pub async fn list_api_keys(
345 State(state): State<AppState>,
346 ctx: Option<Extension<TenantContext>>,
347) -> Result<Json<Vec<V1ApiKey>>> {
348 let tc = extract_tenant(ctx)?;
349 let keys = state.tenant_db.list_api_keys(&tc.tenant_id).await?;
350
351 let response: Vec<V1ApiKey> = keys
352 .into_iter()
353 .filter(|k| k.is_active)
354 .map(|k| V1ApiKey {
355 id: k.id,
356 name: k.name,
357 prefix: k.key_prefix,
358 created_at: ts_to_dt(k.created_at),
359 last_used: None,
360 expires_at: k.expires_at.map(|e| ts_to_dt(e)),
361 })
362 .collect();
363
364 Ok(Json(response))
365}
366
367pub async fn create_api_key(
369 State(state): State<AppState>,
370 ctx: Option<Extension<TenantContext>>,
371 Json(payload): Json<CreateApiKeyRequest>,
372) -> Result<Json<CreateApiKeyResponse>> {
373 let tc = extract_tenant(ctx)?;
374 let (api_key, raw_key) = state
375 .tenant_db
376 .create_api_key(&tc.tenant_id, payload.name)
377 .await?;
378
379 Ok(Json(CreateApiKeyResponse {
380 key: V1ApiKey {
381 id: api_key.id,
382 name: api_key.name,
383 prefix: api_key.key_prefix,
384 created_at: ts_to_dt(api_key.created_at),
385 last_used: None,
386 expires_at: api_key.expires_at.map(|e| ts_to_dt(e)),
387 },
388 secret: raw_key,
389 }))
390}
391
392pub async fn revoke_api_key(
394 State(state): State<AppState>,
395 ctx: Option<Extension<TenantContext>>,
396 Path(key_id): Path<String>,
397) -> Result<StatusCode> {
398 let tc = extract_tenant(ctx)?;
399 state
400 .tenant_db
401 .revoke_api_key(&tc.tenant_id, &key_id)
402 .await?;
403 Ok(StatusCode::NO_CONTENT)
404}