use std::sync::Arc;
use async_trait::async_trait;
use serde_json::{Value, json};
use crate::domain::error::Result;
use crate::ports::agent_source::{AgentRequest, AgentResponse, AgentSourcePort};
use crate::ports::{AIProvider, ScrapingService, ServiceInput, ServiceOutput};
pub struct AgentSource {
provider: Arc<dyn AIProvider>,
}
impl AgentSource {
#[must_use]
pub fn new(provider: Arc<dyn AIProvider>) -> Self {
Self { provider }
}
}
#[async_trait]
impl AgentSourcePort for AgentSource {
async fn invoke(&self, request: AgentRequest) -> Result<AgentResponse> {
let content = match &request.context {
Some(ctx) => format!("{}\n\n---\n\n{ctx}", request.prompt),
None => request.prompt.clone(),
};
let schema = if request.parameters.is_null()
|| request.parameters.is_object()
&& request
.parameters
.as_object()
.is_some_and(serde_json::Map::is_empty)
{
json!({"type": "object", "properties": {"response": {"type": "string"}}})
} else {
request.parameters.clone()
};
let result = self.provider.extract(content, schema).await?;
let content_text = result.get("response").and_then(Value::as_str).map_or_else(
|| serde_json::to_string(&result).unwrap_or_default(),
str::to_owned,
);
Ok(AgentResponse {
content: content_text,
metadata: json!({
"provider": self.provider.name(),
"raw_output": result,
}),
})
}
fn source_name(&self) -> &'static str {
"agent"
}
}
#[async_trait]
impl ScrapingService for AgentSource {
async fn execute(&self, input: ServiceInput) -> Result<ServiceOutput> {
let prompt = input
.params
.get("prompt")
.and_then(Value::as_str)
.unwrap_or("Process the following data")
.to_string();
let context = input
.params
.get("context")
.and_then(Value::as_str)
.map(String::from);
let parameters = input
.params
.get("parameters")
.cloned()
.unwrap_or_else(|| json!({}));
let request = AgentRequest {
prompt,
context,
parameters,
};
let response = self.invoke(request).await?;
Ok(ServiceOutput {
data: response.content,
metadata: response.metadata,
})
}
fn name(&self) -> &'static str {
"agent"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::adapters::mock_ai::MockAIProvider;
fn make_agent() -> AgentSource {
AgentSource::new(Arc::new(MockAIProvider))
}
#[tokio::test]
async fn invoke_returns_response() -> std::result::Result<(), Box<dyn std::error::Error>> {
let agent = make_agent();
let req = AgentRequest {
prompt: "Say hello".into(),
context: None,
parameters: json!({}),
};
let resp = agent.invoke(req).await?;
assert!(!resp.content.is_empty());
assert_eq!(
resp.metadata.get("provider").and_then(Value::as_str),
Some("mock-ai")
);
Ok(())
}
#[tokio::test]
async fn invoke_with_context() -> std::result::Result<(), Box<dyn std::error::Error>> {
let agent = make_agent();
let req = AgentRequest {
prompt: "Summarise".into(),
context: Some("some article text".into()),
parameters: json!({}),
};
let resp = agent.invoke(req).await?;
assert!(!resp.content.is_empty());
Ok(())
}
#[tokio::test]
async fn scraping_service_execute() -> std::result::Result<(), Box<dyn std::error::Error>> {
let agent = make_agent();
let input = ServiceInput {
url: String::new(),
params: json!({
"prompt": "Generate a summary",
}),
};
let output = agent.execute(input).await?;
assert!(!output.data.is_empty());
assert_eq!(
output.metadata.get("provider").and_then(Value::as_str),
Some("mock-ai")
);
Ok(())
}
#[test]
fn source_name() {
let agent = make_agent();
assert_eq!(AgentSourcePort::source_name(&agent), "agent");
}
#[test]
fn service_name() {
let agent = make_agent();
assert_eq!(ScrapingService::name(&agent), "agent");
}
}