smooth_operator_adapter_postgres/
agent_config.rs1use async_trait::async_trait;
16use deadpool_postgres::Pool;
17use tracing::debug;
18
19use smooth_operator::agent_config::{AgentBehaviorConfig, AgentConfigResolver};
20
21#[derive(Clone)]
23pub struct PgAgentConfigResolver {
24 pool: Pool,
25}
26
27impl PgAgentConfigResolver {
28 #[must_use]
30 pub fn new(pool: Pool) -> Self {
31 Self { pool }
32 }
33
34 async fn fetch(&self, agent_id: &str) -> Option<AgentBehaviorConfig> {
36 let id = match uuid::Uuid::parse_str(agent_id) {
40 Ok(id) => id,
41 Err(_) => {
42 debug!(agent_id, "agent_id is not a uuid; no per-agent config");
43 return None;
44 }
45 };
46
47 let client = match self.pool.get().await {
48 Ok(c) => c,
49 Err(e) => {
50 debug!(error = %e, "agent config: pool.get failed; falling back to org default");
51 return None;
52 }
53 };
54
55 let row = match client
56 .query_opt(
57 "SELECT instructions, personality, greeting, conversation_workflow, tool_config, visibility \
58 FROM agents WHERE id = $1",
59 &[&id],
60 )
61 .await
62 {
63 Ok(row) => row?,
64 Err(e) => {
65 debug!(error = %e, agent_id, "agent config query failed; falling back to org default");
67 return None;
68 }
69 };
70
71 let instructions: Option<serde_json::Value> = row.try_get("instructions").ok().flatten();
73 let personality: Option<serde_json::Value> = row.try_get("personality").ok().flatten();
74 let greeting: Option<String> = row.try_get("greeting").ok().flatten();
75 let workflow: Option<serde_json::Value> =
76 row.try_get("conversation_workflow").ok().flatten();
77 let tool_config: Option<serde_json::Value> = row.try_get("tool_config").ok().flatten();
78 let visibility: Option<String> = row.try_get("visibility").ok().flatten();
79
80 let config = AgentBehaviorConfig::from_row_values(
81 instructions,
82 personality,
83 greeting,
84 workflow,
85 tool_config,
86 visibility,
87 );
88 if config.is_empty() {
89 None
90 } else {
91 Some(config)
92 }
93 }
94}
95
96#[async_trait]
97impl AgentConfigResolver for PgAgentConfigResolver {
98 async fn resolve(&self, agent_id: &str) -> Option<AgentBehaviorConfig> {
99 self.fetch(agent_id).await
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[tokio::test]
112 async fn non_uuid_agent_id_is_none_without_touching_db() {
113 let mut cfg = deadpool_postgres::Config::new();
116 cfg.host = Some("127.0.0.1".to_string());
117 cfg.port = Some(1); cfg.dbname = Some("nope".to_string());
119 cfg.user = Some("nobody".to_string());
120 let pool = cfg
121 .create_pool(
122 Some(deadpool_postgres::Runtime::Tokio1),
123 tokio_postgres::NoTls,
124 )
125 .expect("build pool");
126 let provider = PgAgentConfigResolver::new(pool);
127 assert!(provider.resolve("not-a-uuid").await.is_none());
128 }
129}