use std::collections::HashMap;
use std::sync::Arc;
use anyhow::Result;
use async_trait::async_trait;
use serde_json::Value;
pub struct AgentContext {
pub messages: Vec<serde_json::Value>,
pub system_prompt_parts: Vec<String>,
pub tools_schema: Option<Value>,
pub metadata: HashMap<String, Value>,
pub session_id: String,
}
impl AgentContext {
pub fn new(session_id: impl Into<String>) -> Self {
Self {
messages: vec![],
system_prompt_parts: vec![],
tools_schema: None,
metadata: HashMap::new(),
session_id: session_id.into(),
}
}
}
#[async_trait]
pub trait RequestProcessor: Send + Sync {
async fn process(&self, ctx: &mut AgentContext) -> Result<()>;
fn name(&self) -> &str;
}
#[async_trait]
pub trait ResponseProcessor: Send + Sync {
async fn process(&self, ctx: &mut AgentContext, response: &mut Value) -> Result<()>;
fn name(&self) -> &str;
}
pub struct ProcessorPipeline {
request_processors: Vec<Arc<dyn RequestProcessor>>,
response_processors: Vec<Arc<dyn ResponseProcessor>>,
}
impl ProcessorPipeline {
pub fn new() -> Self {
Self {
request_processors: vec![],
response_processors: vec![],
}
}
pub fn add_request_processor(mut self, p: Arc<dyn RequestProcessor>) -> Self {
self.request_processors.push(p);
self
}
pub fn add_response_processor(mut self, p: Arc<dyn ResponseProcessor>) -> Self {
self.response_processors.push(p);
self
}
pub async fn run_request_processors(&self, ctx: &mut AgentContext) -> Result<()> {
for p in &self.request_processors {
tracing::debug!(processor = p.name(), "running request processor");
p.process(ctx).await?;
}
Ok(())
}
pub async fn run_response_processors(&self, ctx: &mut AgentContext, response: &mut Value) -> Result<()> {
for p in &self.response_processors {
tracing::debug!(processor = p.name(), "running response processor");
p.process(ctx, response).await?;
}
Ok(())
}
}
impl Default for ProcessorPipeline {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_agent_context_new() {
let ctx = AgentContext::new("session-1");
assert_eq!(ctx.session_id, "session-1");
assert!(ctx.messages.is_empty());
assert!(ctx.system_prompt_parts.is_empty());
assert!(ctx.tools_schema.is_none());
assert!(ctx.metadata.is_empty());
}
#[test]
fn test_agent_context_new_with_string() {
let ctx = AgentContext::new(String::from("s2"));
assert_eq!(ctx.session_id, "s2");
}
struct TestRequestProcessor {
name_val: String,
}
#[async_trait]
impl RequestProcessor for TestRequestProcessor {
async fn process(&self, ctx: &mut AgentContext) -> Result<()> {
ctx.system_prompt_parts.push(format!("processed by {}", self.name_val));
Ok(())
}
fn name(&self) -> &str { &self.name_val }
}
struct TestResponseProcessor;
#[async_trait]
impl ResponseProcessor for TestResponseProcessor {
async fn process(&self, ctx: &mut AgentContext, response: &mut Value) -> Result<()> {
ctx.metadata.insert("processed".into(), Value::Bool(true));
*response = Value::String("modified".into());
Ok(())
}
fn name(&self) -> &str { "test-response" }
}
struct FailingProcessor;
#[async_trait]
impl RequestProcessor for FailingProcessor {
async fn process(&self, _ctx: &mut AgentContext) -> Result<()> {
anyhow::bail!("intentional failure")
}
fn name(&self) -> &str { "failing" }
}
#[tokio::test]
async fn test_pipeline_empty() {
let pipeline = ProcessorPipeline::new();
let mut ctx = AgentContext::new("s1");
pipeline.run_request_processors(&mut ctx).await.unwrap();
let mut resp = Value::Null;
pipeline.run_response_processors(&mut ctx, &mut resp).await.unwrap();
assert!(ctx.system_prompt_parts.is_empty());
assert_eq!(resp, Value::Null);
}
#[tokio::test]
async fn test_pipeline_default() {
let pipeline = ProcessorPipeline::default();
let mut ctx = AgentContext::new("s1");
pipeline.run_request_processors(&mut ctx).await.unwrap();
assert!(ctx.system_prompt_parts.is_empty());
}
#[tokio::test]
async fn test_pipeline_request_processor() {
let pipeline = ProcessorPipeline::new()
.add_request_processor(Arc::new(TestRequestProcessor { name_val: "rp1".into() }));
let mut ctx = AgentContext::new("s1");
pipeline.run_request_processors(&mut ctx).await.unwrap();
assert_eq!(ctx.system_prompt_parts.len(), 1);
assert_eq!(ctx.system_prompt_parts[0], "processed by rp1");
}
#[tokio::test]
async fn test_pipeline_response_processor() {
let pipeline = ProcessorPipeline::new()
.add_response_processor(Arc::new(TestResponseProcessor));
let mut ctx = AgentContext::new("s1");
let mut resp = Value::Null;
pipeline.run_response_processors(&mut ctx, &mut resp).await.unwrap();
assert_eq!(ctx.metadata.get("processed").unwrap(), &Value::Bool(true));
assert_eq!(resp, Value::String("modified".into()));
}
#[tokio::test]
async fn test_pipeline_chained_request_processors() {
let pipeline = ProcessorPipeline::new()
.add_request_processor(Arc::new(TestRequestProcessor { name_val: "first".into() }))
.add_request_processor(Arc::new(TestRequestProcessor { name_val: "second".into() }));
let mut ctx = AgentContext::new("s1");
pipeline.run_request_processors(&mut ctx).await.unwrap();
assert_eq!(ctx.system_prompt_parts.len(), 2);
assert_eq!(ctx.system_prompt_parts[0], "processed by first");
assert_eq!(ctx.system_prompt_parts[1], "processed by second");
}
#[tokio::test]
async fn test_pipeline_request_processor_failure_stops() {
let pipeline = ProcessorPipeline::new()
.add_request_processor(Arc::new(FailingProcessor))
.add_request_processor(Arc::new(TestRequestProcessor { name_val: "never".into() }));
let mut ctx = AgentContext::new("s1");
let result = pipeline.run_request_processors(&mut ctx).await;
assert!(result.is_err());
assert!(ctx.system_prompt_parts.is_empty());
}
#[tokio::test]
async fn test_pipeline_builder_chaining() {
let pipeline = ProcessorPipeline::new()
.add_request_processor(Arc::new(TestRequestProcessor { name_val: "rp".into() }))
.add_response_processor(Arc::new(TestResponseProcessor));
let mut ctx = AgentContext::new("s1");
pipeline.run_request_processors(&mut ctx).await.unwrap();
let mut resp = Value::Null;
pipeline.run_response_processors(&mut ctx, &mut resp).await.unwrap();
assert_eq!(ctx.system_prompt_parts.len(), 1);
assert_eq!(resp, Value::String("modified".into()));
}
}