espionox
is an attempt to make building Ai applications in Rust just as approachable as it is with other libraries such as LangChain.
- Making an LLM application in Rust
- Experimenting with with complex 'prompt flows' such as Chain/Tree of thought
First you need to initialize an Agent
Agent::new
accepts two arguments:
- Optional content of a system prompt, if this is left
None
your agent will have no system prompt - A
CompletionModel
whichever provider you wish to use (As of writing, only OpenAi and Anthropic providers are supported).
use espionox::prelude::*;
let api_key = std::env::var("OPENAI_KEY").unwrap();
let agent = Agent::new(Some("This is the system message"), CompletionModel::default_openai(api_key));
Now, In order to prompt your agent you will call do_action
on it
let response: String = agent
.do_action(io_completion, (), Option::<ListenerTrigger>::None)
.await
.unwrap();
This may look scary at first, but lets look at do_action
's signature:
pub async fn do_action<'a, F, Args, Fut, R>(
&'a mut self,
f: F,
args: Args,
trigger: Option<impl Into<ListenerTrigger>>,
) -> AgentResult<R>
where
F: for<'l> FnOnce(&'a mut Agent, Args) -> Fut,
Fut: Future<Output = AgentResult<R>>
do_action
takes 4 arguments:
- the
Agent
which calls the method - an async function which mutates the agent and returns an
AgentResult
, which can be coerced from ananyhow::Result
. So as long as the function signature returns anyAgentResult<T>
, just make sure to call.into()
on any error return and it should be valid. - optionally arguments for the aformentioned function
- An optional trigger for a listener (We'll get to this)
So, in our call to do_action
earlier, we passed the function io_completion
, an empty argument and None.
espionox
provides the following helper functions for getting completions or embeddings:
io_completion
stream_completion
function_completion
We used one of these functions, but we could have just as easily defined our own io_completion
function and passed it when we called do_action
One of Espionox's best offerings is the AgentListener
trait:
pub trait AgentListener: std::fmt::Debug + Send + Sync + 'static {
fn trigger<'l>(&self) -> ListenerTrigger;
fn async_method<'l>(&'l mut self, _a: &'l mut Agent) -> ListenerCallReturn<'l> {
Box::pin(async move { Err(ListenerError::NoMethod.into()) })
}
fn sync_method<'l>(&'l mut self, _a: &'l mut Agent) -> AgentResult<()> {
Err(ListenerError::NoMethod.into())
}
}
You will notice 3 methods:
trigger
: this is how you define when the listener will be triggered. Think of it like an ID.ListenerTrigger
has 2 variants:ListenerTrigger::String(String)
ListenerTrigger::Int(i64)
Remember thetrigger
argument todo_action
? Ensure a listener is triggered whendo_action
is called by passing a matchingListenerTrigger
.
async_method
. Which, if implemented, the function body must be wrapped inBox::pin(async move {})
sync_method
Each async_method
and sync_method
are where you define WHAT the listener will actually do when it's triggered. THESE ARE MUTUALLY EXCLUSIVE, only ONE of these methods should be implemented. If both are implemented, the sync method will be the only one to trigger.
Any struct implementing this trait can be inserted into an agent using Agent::insert_listener
.
The utility of listeners may not be immediately obvious to you, but it can be used to create self consistency mechanisms, prompt chains or even RAG pipelines.
Check the examples directory for more information on AgentListener
espionox is very early in development and everything in the API may be subject to change Please feel free to reach out with any questions, suggestions, issues or anything else :)