use std::collections::HashMap;
use std::sync::Mutex;
use thiserror::Error;
pub trait Translator: Send + Sync + 'static {
fn t(&self, key: &str) -> String;
fn tf(&self, key: &str, args: &[(&str, &str)]) -> String;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct KeyTranslator;
impl Translator for KeyTranslator {
fn t(&self, key: &str) -> String {
key.to_string()
}
fn tf(&self, key: &str, _args: &[(&str, &str)]) -> String {
key.to_string()
}
}
#[derive(Debug, Error)]
pub enum SecretsError {
#[error("secret not found: {0}")]
NotFound(String),
#[error("backend error: {0}")]
Backend(String),
}
pub trait SecretsBackend: Send + Sync + 'static {
fn get(&self, key: &str) -> Result<String, SecretsError>;
}
#[derive(Default)]
pub struct InMemorySecrets {
map: Mutex<HashMap<String, String>>,
}
impl InMemorySecrets {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, key: &str, value: &str) {
let mut g = self
.map
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
g.insert(key.to_string(), value.to_string());
}
}
impl SecretsBackend for InMemorySecrets {
fn get(&self, key: &str) -> Result<String, SecretsError> {
let g = self
.map
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
g.get(key)
.cloned()
.ok_or_else(|| SecretsError::NotFound(key.to_string()))
}
}
#[derive(Debug, Clone)]
pub struct LlmPortRequest {
pub system_prompt: String,
pub messages: Vec<(String, String)>,
pub response_format: LlmPortResponseFormat,
}
#[derive(Debug, Clone, Default)]
pub enum LlmPortResponseFormat {
#[default]
Text,
Json,
JsonSchema(String),
}
#[derive(Debug, Clone)]
pub struct LlmPortResponse {
pub content: String,
pub total_tokens: Option<u32>,
}
#[derive(Debug, Error)]
pub enum LlmPortError {
#[error("llm role unassigned: {0}")]
RoleUnassigned(String),
#[error("backend error: {0}")]
Backend(String),
}
pub trait LlmPort: Send + Sync {
fn complete(
&self,
extension_id: &str,
ctx: &HostCallContext,
role: &str,
request: LlmPortRequest,
) -> Result<LlmPortResponse, LlmPortError>;
}
#[derive(Debug, Clone, Default)]
pub struct HostCallContext {
pub tenant: Option<String>,
pub user_email: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_translator_returns_key_for_t() {
let t = KeyTranslator;
assert_eq!(t.t("greentic.test.hello"), "greentic.test.hello");
}
#[test]
fn key_translator_substitutes_args_for_tf() {
let t = KeyTranslator;
let out = t.tf("greentic.test.hello.{}", &[("name", "Bima")]);
assert_eq!(out, "greentic.test.hello.{}");
}
#[test]
fn in_memory_secrets_returns_value_when_present() {
let mut s = InMemorySecrets::default();
s.insert("api.openai.com/api_key", "sk-test");
let v = s.get("api.openai.com/api_key").unwrap();
assert_eq!(v, "sk-test");
}
#[test]
fn in_memory_secrets_returns_not_found_when_absent() {
let s = InMemorySecrets::default();
let err = s.get("api.openai.com/api_key").unwrap_err();
assert!(matches!(err, SecretsError::NotFound(_)));
}
}