1use std::sync::Arc;
4
5use convergio_db::pool::ConnPool;
6use convergio_types::extension::{AppContext, Extension, Health, McpToolDef, Metric, Migration};
7use convergio_types::manifest::{Capability, Manifest, ModuleKind};
8
9use crate::routes::{reports_routes, ReportsState};
10
11pub struct ReportsExtension {
12 pool: ConnPool,
13}
14
15impl ReportsExtension {
16 pub fn new(pool: ConnPool) -> Self {
17 Self { pool }
18 }
19
20 fn state(&self) -> Arc<ReportsState> {
21 Arc::new(ReportsState {
22 pool: self.pool.clone(),
23 })
24 }
25}
26
27impl Extension for ReportsExtension {
28 fn manifest(&self) -> Manifest {
29 Manifest {
30 id: "convergio-reports".to_string(),
31 description: "Convergio Think Tank — professional report generation service"
32 .to_string(),
33 version: env!("CARGO_PKG_VERSION").to_string(),
34 kind: ModuleKind::Extension,
35 provides: vec![
36 Capability {
37 name: "reports".to_string(),
38 version: "1.0.0".to_string(),
39 description: "CTT research report generation".to_string(),
40 },
41 Capability {
42 name: "reports-api".to_string(),
43 version: "1.0.0".to_string(),
44 description: "REST API for report CRUD and generation".to_string(),
45 },
46 ],
47 requires: vec![],
48 agent_tools: vec![],
49 required_roles: vec!["orchestrator".into(), "all".into()],
50 }
51 }
52
53 fn routes(&self, _ctx: &AppContext) -> Option<axum::Router> {
54 Some(reports_routes(self.state()))
55 }
56
57 fn migrations(&self) -> Vec<Migration> {
58 vec![Migration {
59 version: 1,
60 description: "reports table",
61 up: "CREATE TABLE IF NOT EXISTS reports (\
62 id TEXT PRIMARY KEY,\
63 org_id TEXT NOT NULL DEFAULT 'convergio.io',\
64 topic TEXT NOT NULL,\
65 report_type TEXT NOT NULL,\
66 format TEXT NOT NULL DEFAULT 'markdown',\
67 status TEXT NOT NULL DEFAULT 'pending',\
68 audience TEXT,\
69 depth TEXT NOT NULL DEFAULT 'standard',\
70 content_md TEXT,\
71 pdf_path TEXT,\
72 sources_json TEXT,\
73 metadata_json TEXT,\
74 extra_context TEXT,\
75 error TEXT,\
76 created_at TEXT NOT NULL DEFAULT (datetime('now')),\
77 completed_at TEXT\
78 );\
79 CREATE INDEX IF NOT EXISTS idx_reports_org \
80 ON reports(org_id);\
81 CREATE INDEX IF NOT EXISTS idx_reports_status \
82 ON reports(status);\
83 CREATE INDEX IF NOT EXISTS idx_reports_type \
84 ON reports(report_type);",
85 }]
86 }
87
88 fn health(&self) -> Health {
89 match self.pool.get() {
90 Ok(_) => Health::Ok,
91 Err(e) => Health::Degraded {
92 reason: format!("db: {e}"),
93 },
94 }
95 }
96
97 fn metrics(&self) -> Vec<Metric> {
98 let count: f64 = self
99 .pool
100 .get()
101 .ok()
102 .and_then(|c| {
103 c.query_row("SELECT COUNT(*) FROM reports", [], |r| r.get::<_, i64>(0))
104 .ok()
105 })
106 .unwrap_or(0) as f64;
107 vec![Metric {
108 name: "reports_total".to_string(),
109 value: count,
110 labels: vec![],
111 }]
112 }
113
114 fn mcp_tools(&self) -> Vec<McpToolDef> {
115 crate::mcp_defs::reports_tools()
116 }
117}