use std::sync::Arc;
use async_trait::async_trait;
use converge_pack::{
AgentEffect, Context, ContextKey, ExecutionIdentity, FactPayload, ProposedFact,
ProvenanceSource, Suggestor,
};
use serde::{Deserialize, Serialize};
use crate::provenance::GITHUB_PROVENANCE;
use crate::provider::{GithubProvider, GithubRequest};
use crate::types::Organization;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GithubOrganizationPayload {
pub organization: Organization,
pub request_hash: String,
pub vendor: String,
pub latency_ms: u64,
pub execution_identity: ExecutionIdentity,
}
impl FactPayload for GithubOrganizationPayload {
const FAMILY: &'static str = "embassy.github.organization";
const VERSION: u16 = 1;
}
pub struct GithubLookupSuggestor<P: GithubProvider + 'static> {
provider: Arc<P>,
}
impl<P: GithubProvider + 'static> GithubLookupSuggestor<P> {
pub fn new(provider: Arc<P>) -> Self {
Self { provider }
}
}
#[async_trait]
impl<P: GithubProvider + 'static> Suggestor for GithubLookupSuggestor<P> {
fn name(&self) -> &'static str {
"GithubLookupSuggestor"
}
fn dependencies(&self) -> &[ContextKey] {
&[ContextKey::Seeds]
}
fn provenance(&self) -> &'static str {
GITHUB_PROVENANCE.as_str()
}
fn accepts(&self, ctx: &dyn Context) -> bool {
ctx.get(ContextKey::Seeds)
.iter()
.any(|fact| fact.payload::<GithubRequest>().is_some())
}
async fn execute(&self, ctx: &dyn Context) -> AgentEffect {
let mut proposals = Vec::new();
for seed in ctx.get(ContextKey::Seeds) {
let Some(request) = seed.payload::<GithubRequest>() else {
continue;
};
let response = match self
.provider
.fetch(request, &embassy_pack::CallContext::default())
.await
{
Ok(resp) => resp,
Err(err) => {
tracing::warn!(
seed = %seed.id(),
provider = self.provider.name(),
error = %err,
"github fetch failed; skipping seed"
);
continue;
}
};
for (idx, observation) in response.records.into_iter().enumerate() {
let runtime_config = ExecutionIdentity::runtime_config_from_typed(request);
let execution_identity = ExecutionIdentity::non_native(
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
self.provider.name().to_string(),
runtime_config,
);
let payload_value = GithubOrganizationPayload {
organization: observation.content,
request_hash: observation.request_hash,
vendor: observation.vendor,
latency_ms: observation.latency_ms,
execution_identity,
};
proposals.push(
ProposedFact::new(
ContextKey::Hypotheses,
format!("github:{}:{idx}", seed.id()),
payload_value,
GITHUB_PROVENANCE.as_str(),
)
.with_confidence(0.95),
);
}
}
if proposals.is_empty() {
AgentEffect::empty()
} else {
AgentEffect::with_proposals(proposals)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::provider::StubGithubProvider;
#[test]
fn suggestor_declares_seeds_dependency() {
let s = GithubLookupSuggestor::new(Arc::new(StubGithubProvider));
assert_eq!(s.dependencies(), &[ContextKey::Seeds]);
}
#[test]
fn suggestor_provenance_is_canonical() {
let s = GithubLookupSuggestor::new(Arc::new(StubGithubProvider));
assert_eq!(s.provenance(), GITHUB_PROVENANCE.as_str());
}
#[test]
fn payload_family_and_version_are_stable() {
assert_eq!(
GithubOrganizationPayload::FAMILY,
"embassy.github.organization"
);
assert_eq!(GithubOrganizationPayload::VERSION, 1);
}
}