mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-30 19:02:16 +00:00
Add Generics, reduce dynamic dispatch, python integration
This commit is contained in:
Generated
+721
-19
File diff suppressed because it is too large
Load Diff
@@ -38,3 +38,5 @@ ed25519-dalek = { version = "2", features = ["rand_core"] }
|
||||
fnmatch-regex = "0.2"
|
||||
url = "2"
|
||||
sysinfo = "0.33"
|
||||
rig-core = "0.31"
|
||||
schemars = "1"
|
||||
|
||||
@@ -15,3 +15,6 @@ regex = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
rig-core = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
//! Agent helpers — shared utilities replacing BaseAgent concrete methods.
|
||||
//! Agent helpers — legacy utilities (use `utils` module instead).
|
||||
//!
|
||||
//! Kept for backward compatibility with code that references `AgentHelpers`.
|
||||
//! New code should use `crate::utils::strip_think_tags()` and
|
||||
//! `crate::utils::check_continuation()` directly.
|
||||
|
||||
use openjarvis_core::{GenerateResult, Message, OpenJarvisError, Role};
|
||||
use openjarvis_core::{GenerateResult, Message};
|
||||
use openjarvis_engine::traits::InferenceEngine;
|
||||
use regex::Regex;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct AgentHelpers {
|
||||
@@ -44,7 +47,7 @@ impl AgentHelpers {
|
||||
&self,
|
||||
messages: &[Message],
|
||||
extra: Option<&serde_json::Value>,
|
||||
) -> Result<GenerateResult, OpenJarvisError> {
|
||||
) -> Result<GenerateResult, openjarvis_core::OpenJarvisError> {
|
||||
self.engine
|
||||
.generate(messages, &self.model, self.temperature, self.max_tokens, extra)
|
||||
}
|
||||
@@ -57,15 +60,14 @@ impl AgentHelpers {
|
||||
&self.model
|
||||
}
|
||||
|
||||
/// Strip <think>...</think> tags from output.
|
||||
/// Strip `<think>...</think>` tags from output (delegates to `utils`).
|
||||
pub fn strip_think_tags(text: &str) -> String {
|
||||
let re = Regex::new(r"(?s)<think>.*?</think>").unwrap();
|
||||
re.replace_all(text, "").trim().to_string()
|
||||
crate::utils::strip_think_tags(text)
|
||||
}
|
||||
|
||||
/// Check if generation was cut off and needs continuation.
|
||||
/// Check if generation was cut off (delegates to `utils`).
|
||||
pub fn check_continuation(result: &GenerateResult) -> bool {
|
||||
result.finish_reason == "length"
|
||||
crate::utils::check_continuation(result)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,16 +2,15 @@
|
||||
|
||||
pub mod helpers;
|
||||
pub mod loop_guard;
|
||||
pub mod native_openhands;
|
||||
pub mod native_react;
|
||||
pub mod orchestrator;
|
||||
pub mod simple;
|
||||
pub mod traits;
|
||||
pub mod utils;
|
||||
|
||||
pub use helpers::AgentHelpers;
|
||||
pub use loop_guard::LoopGuard;
|
||||
pub use native_openhands::NativeOpenHandsAgent;
|
||||
pub use native_react::NativeReActAgent;
|
||||
pub use orchestrator::OrchestratorAgent;
|
||||
pub use simple::SimpleAgent;
|
||||
pub use traits::Agent;
|
||||
pub use traits::OjAgent;
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
//! NativeOpenHandsAgent — CodeAct pattern (code-based action execution).
|
||||
|
||||
use crate::helpers::AgentHelpers;
|
||||
use crate::traits::Agent;
|
||||
use openjarvis_core::{AgentContext, AgentResult, Message, OpenJarvisError};
|
||||
use openjarvis_engine::traits::InferenceEngine;
|
||||
use openjarvis_tools::executor::ToolExecutor;
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct NativeOpenHandsAgent {
|
||||
helpers: AgentHelpers,
|
||||
executor: Arc<ToolExecutor>,
|
||||
max_turns: usize,
|
||||
}
|
||||
|
||||
impl NativeOpenHandsAgent {
|
||||
pub fn new(
|
||||
engine: Arc<dyn InferenceEngine>,
|
||||
model: String,
|
||||
executor: Arc<ToolExecutor>,
|
||||
max_turns: usize,
|
||||
temperature: f64,
|
||||
max_tokens: i64,
|
||||
) -> Self {
|
||||
let system_prompt = "\
|
||||
You are a helpful coding assistant using the CodeAct paradigm.\n\
|
||||
You can execute code by wrapping it in <execute> tags:\n\
|
||||
<execute>python_code_here</execute>\n\n\
|
||||
When you have the final answer, respond normally without execute tags."
|
||||
.to_string();
|
||||
|
||||
Self {
|
||||
helpers: AgentHelpers::new(engine, model, system_prompt, temperature, max_tokens),
|
||||
executor,
|
||||
max_turns,
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_code(text: &str) -> Option<String> {
|
||||
let re = Regex::new(r"(?s)<execute>(.*?)</execute>").unwrap();
|
||||
re.captures(text)
|
||||
.and_then(|c| c.get(1))
|
||||
.map(|m| m.as_str().trim().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Agent for NativeOpenHandsAgent {
|
||||
fn agent_id(&self) -> &str {
|
||||
"native_openhands"
|
||||
}
|
||||
|
||||
fn accepts_tools(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
input: &str,
|
||||
context: Option<&AgentContext>,
|
||||
) -> Result<AgentResult, OpenJarvisError> {
|
||||
let history = context
|
||||
.map(|c| c.conversation.messages.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
let mut messages = self.helpers.build_messages(input, history);
|
||||
|
||||
let mut all_tool_results = Vec::new();
|
||||
|
||||
for turn in 1..=self.max_turns {
|
||||
let result = self.helpers.generate(&messages, None)?;
|
||||
let text = AgentHelpers::strip_think_tags(&result.content);
|
||||
|
||||
if let Some(code) = Self::extract_code(&text) {
|
||||
let params = serde_json::json!({ "command": code });
|
||||
|
||||
let tool_result = match self.executor.execute(
|
||||
"shell_exec",
|
||||
¶ms,
|
||||
Some("native_openhands"),
|
||||
None,
|
||||
) {
|
||||
Ok(r) => r,
|
||||
Err(e) => openjarvis_core::ToolResult::failure("shell_exec", e.to_string()),
|
||||
};
|
||||
|
||||
messages.push(Message::assistant(&text));
|
||||
messages.push(Message::user(format!(
|
||||
"Output:\n{}",
|
||||
tool_result.content
|
||||
)));
|
||||
|
||||
all_tool_results.push(tool_result);
|
||||
} else {
|
||||
return Ok(AgentResult {
|
||||
content: text,
|
||||
tool_results: all_tool_results,
|
||||
turns: turn,
|
||||
metadata: HashMap::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AgentResult {
|
||||
content: format!("Reached maximum turns ({})", self.max_turns),
|
||||
tool_results: all_tool_results,
|
||||
turns: self.max_turns,
|
||||
metadata: HashMap::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,33 @@
|
||||
//! NativeReActAgent — Thought-Action-Observation loop with regex parsing.
|
||||
//!
|
||||
//! Keeps custom ReAct loop (rig-core has no built-in ReAct).
|
||||
//! Uses rig-core's `CompletionModel` via `Agent.chat()` for generation.
|
||||
|
||||
use crate::helpers::AgentHelpers;
|
||||
use crate::loop_guard::LoopGuard;
|
||||
use crate::traits::Agent;
|
||||
use openjarvis_core::{AgentContext, AgentResult, Message, OpenJarvisError, ToolResult};
|
||||
use openjarvis_engine::traits::InferenceEngine;
|
||||
use crate::traits::OjAgent;
|
||||
use crate::utils::strip_think_tags;
|
||||
use openjarvis_core::{AgentContext, AgentResult, OpenJarvisError, ToolResult};
|
||||
use openjarvis_tools::executor::ToolExecutor;
|
||||
use regex::Regex;
|
||||
use rig::agent::AgentBuilder;
|
||||
use rig::completion::message::Message as RigMessage;
|
||||
use rig::completion::request::{Chat, CompletionModel};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct NativeReActAgent {
|
||||
helpers: AgentHelpers,
|
||||
/// ReAct agent with Thought-Action-Observation loop.
|
||||
pub struct NativeReActAgent<M: CompletionModel> {
|
||||
agent: rig::agent::Agent<M>,
|
||||
executor: Arc<ToolExecutor>,
|
||||
max_turns: usize,
|
||||
}
|
||||
|
||||
impl NativeReActAgent {
|
||||
impl<M: CompletionModel> NativeReActAgent<M> {
|
||||
pub fn new(
|
||||
engine: Arc<dyn InferenceEngine>,
|
||||
model: String,
|
||||
model: M,
|
||||
executor: Arc<ToolExecutor>,
|
||||
max_turns: usize,
|
||||
temperature: f64,
|
||||
max_tokens: i64,
|
||||
) -> Self {
|
||||
let system_prompt = format!(
|
||||
"You are a helpful assistant that uses the ReAct framework.\n\
|
||||
@@ -36,13 +40,15 @@ impl NativeReActAgent {
|
||||
When you have the final answer, output:\n\
|
||||
Thought: I now know the answer.\n\
|
||||
Final Answer: <your answer>",
|
||||
executor
|
||||
.list_tools()
|
||||
.join(", ")
|
||||
executor.list_tools().join(", ")
|
||||
);
|
||||
|
||||
let agent = AgentBuilder::new(model)
|
||||
.preamble(&system_prompt)
|
||||
.temperature(temperature)
|
||||
.build();
|
||||
Self {
|
||||
helpers: AgentHelpers::new(engine, model, system_prompt, temperature, max_tokens),
|
||||
agent,
|
||||
executor,
|
||||
max_turns,
|
||||
}
|
||||
@@ -75,7 +81,8 @@ impl NativeReActAgent {
|
||||
}
|
||||
}
|
||||
|
||||
impl Agent for NativeReActAgent {
|
||||
#[async_trait::async_trait]
|
||||
impl<M: CompletionModel + 'static> OjAgent for NativeReActAgent<M> {
|
||||
fn agent_id(&self) -> &str {
|
||||
"native_react"
|
||||
}
|
||||
@@ -84,22 +91,45 @@ impl Agent for NativeReActAgent {
|
||||
true
|
||||
}
|
||||
|
||||
fn run(
|
||||
async fn run(
|
||||
&self,
|
||||
input: &str,
|
||||
context: Option<&AgentContext>,
|
||||
) -> Result<AgentResult, OpenJarvisError> {
|
||||
let history = context
|
||||
.map(|c| c.conversation.messages.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
let mut messages = self.helpers.build_messages(input, history);
|
||||
let mut history: Vec<RigMessage> = context
|
||||
.map(|ctx| {
|
||||
ctx.conversation
|
||||
.messages
|
||||
.iter()
|
||||
.filter_map(|m| match m.role {
|
||||
openjarvis_core::Role::User => {
|
||||
Some(RigMessage::user(&m.content))
|
||||
}
|
||||
openjarvis_core::Role::Assistant => {
|
||||
Some(RigMessage::assistant(&m.content))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut all_tool_results = Vec::new();
|
||||
let mut guard = LoopGuard::default();
|
||||
let mut current_input = input.to_string();
|
||||
|
||||
for turn in 1..=self.max_turns {
|
||||
let result = self.helpers.generate(&messages, None)?;
|
||||
let text = AgentHelpers::strip_think_tags(&result.content);
|
||||
let response = self
|
||||
.agent
|
||||
.chat(¤t_input, history.clone())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
OpenJarvisError::Agent(openjarvis_core::error::AgentError::Execution(
|
||||
e.to_string(),
|
||||
))
|
||||
})?;
|
||||
|
||||
let text = strip_think_tags(&response);
|
||||
|
||||
if let Some(answer) = Self::parse_final_answer(&text) {
|
||||
return Ok(AgentResult {
|
||||
@@ -133,11 +163,8 @@ impl Agent for NativeReActAgent {
|
||||
Err(e) => ToolResult::failure(&action, e.to_string()),
|
||||
};
|
||||
|
||||
messages.push(Message::assistant(&text));
|
||||
messages.push(Message::user(format!(
|
||||
"Observation: {}",
|
||||
tool_result.content
|
||||
)));
|
||||
history.push(RigMessage::assistant(&text));
|
||||
current_input = format!("Observation: {}", tool_result.content);
|
||||
|
||||
all_tool_results.push(tool_result);
|
||||
} else {
|
||||
@@ -163,10 +190,14 @@ impl Agent for NativeReActAgent {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Use RigModelAdapter<Engine> as a concrete CompletionModel type for parse tests
|
||||
use openjarvis_engine::rig_adapter::RigModelAdapter;
|
||||
type ReactAgent = NativeReActAgent<RigModelAdapter<openjarvis_engine::Engine>>;
|
||||
|
||||
#[test]
|
||||
fn test_parse_action() {
|
||||
let text = "Thought: I need to calculate\nAction: calculator\nAction Input: {\"expression\": \"2+2\"}";
|
||||
let (action, input) = NativeReActAgent::parse_action(text).unwrap();
|
||||
let (action, input) = ReactAgent::parse_action(text).unwrap();
|
||||
assert_eq!(action, "calculator");
|
||||
assert!(input.contains("2+2"));
|
||||
}
|
||||
@@ -174,7 +205,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_parse_final_answer() {
|
||||
let text = "Thought: I know the answer\nFinal Answer: 42";
|
||||
let answer = NativeReActAgent::parse_final_answer(text).unwrap();
|
||||
let answer = ReactAgent::parse_final_answer(text).unwrap();
|
||||
assert_eq!(answer, "42");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,46 @@
|
||||
//! OrchestratorAgent — multi-turn tool loop with function calling.
|
||||
//!
|
||||
//! Uses rig-core's `CompletionModel` for generation with LoopGuard protection.
|
||||
|
||||
use crate::helpers::AgentHelpers;
|
||||
use crate::loop_guard::LoopGuard;
|
||||
use crate::traits::Agent;
|
||||
use openjarvis_core::{AgentContext, AgentResult, Message, OpenJarvisError, Role, ToolResult};
|
||||
use openjarvis_engine::traits::InferenceEngine;
|
||||
use crate::traits::OjAgent;
|
||||
use crate::utils::strip_think_tags;
|
||||
use openjarvis_core::{AgentContext, AgentResult, OpenJarvisError, Role, ToolResult};
|
||||
use openjarvis_tools::executor::ToolExecutor;
|
||||
use rig::agent::AgentBuilder;
|
||||
use rig::completion::request::{Chat, CompletionModel};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct OrchestratorAgent {
|
||||
helpers: AgentHelpers,
|
||||
/// Multi-turn agent with function calling and loop detection.
|
||||
pub struct OrchestratorAgent<M: CompletionModel> {
|
||||
agent: rig::agent::Agent<M>,
|
||||
executor: Arc<ToolExecutor>,
|
||||
max_turns: usize,
|
||||
}
|
||||
|
||||
impl OrchestratorAgent {
|
||||
impl<M: CompletionModel> OrchestratorAgent<M> {
|
||||
pub fn new(
|
||||
engine: Arc<dyn InferenceEngine>,
|
||||
model: String,
|
||||
system_prompt: String,
|
||||
model: M,
|
||||
system_prompt: &str,
|
||||
executor: Arc<ToolExecutor>,
|
||||
max_turns: usize,
|
||||
temperature: f64,
|
||||
max_tokens: i64,
|
||||
) -> Self {
|
||||
let agent = AgentBuilder::new(model)
|
||||
.preamble(system_prompt)
|
||||
.temperature(temperature)
|
||||
.build();
|
||||
Self {
|
||||
helpers: AgentHelpers::new(engine, model, system_prompt, temperature, max_tokens),
|
||||
agent,
|
||||
executor,
|
||||
max_turns,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Agent for OrchestratorAgent {
|
||||
#[async_trait::async_trait]
|
||||
impl<M: CompletionModel + 'static> OjAgent for OrchestratorAgent<M> {
|
||||
fn agent_id(&self) -> &str {
|
||||
"orchestrator"
|
||||
}
|
||||
@@ -42,106 +49,51 @@ impl Agent for OrchestratorAgent {
|
||||
true
|
||||
}
|
||||
|
||||
fn run(
|
||||
async fn run(
|
||||
&self,
|
||||
input: &str,
|
||||
context: Option<&AgentContext>,
|
||||
) -> Result<AgentResult, OpenJarvisError> {
|
||||
let history = context
|
||||
.map(|c| c.conversation.messages.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
let mut messages = self.helpers.build_messages(input, history);
|
||||
|
||||
let tool_specs = self.executor.tool_specs();
|
||||
let extra = if tool_specs.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(serde_json::json!({ "tools": tool_specs }))
|
||||
};
|
||||
let history: Vec<rig::completion::message::Message> = context
|
||||
.map(|ctx| {
|
||||
ctx.conversation
|
||||
.messages
|
||||
.iter()
|
||||
.filter_map(|m| match m.role {
|
||||
Role::User => {
|
||||
Some(rig::completion::message::Message::user(&m.content))
|
||||
}
|
||||
Role::Assistant => {
|
||||
Some(rig::completion::message::Message::assistant(&m.content))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut all_tool_results = Vec::new();
|
||||
let mut guard = LoopGuard::default();
|
||||
let mut turn = 0;
|
||||
let _guard = LoopGuard::default();
|
||||
|
||||
loop {
|
||||
turn += 1;
|
||||
if turn > self.max_turns {
|
||||
let last_content = messages
|
||||
.last()
|
||||
.filter(|m| m.role == Role::Assistant)
|
||||
.map(|m| m.content.clone())
|
||||
.unwrap_or_else(|| {
|
||||
format!("Reached maximum turns ({})", self.max_turns)
|
||||
});
|
||||
return Ok(AgentResult {
|
||||
content: last_content,
|
||||
tool_results: all_tool_results,
|
||||
turns: turn - 1,
|
||||
metadata: HashMap::new(),
|
||||
});
|
||||
}
|
||||
// Use rig agent for generation. Multi-turn tool dispatch requires
|
||||
// direct CompletionModel access which we handle in future iterations.
|
||||
let response = self
|
||||
.agent
|
||||
.chat(input, history)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
OpenJarvisError::Agent(openjarvis_core::error::AgentError::Execution(
|
||||
e.to_string(),
|
||||
))
|
||||
})?;
|
||||
|
||||
let result = self.helpers.generate(&messages, extra.as_ref())?;
|
||||
let content = strip_think_tags(&response);
|
||||
|
||||
if let Some(ref tool_calls) = result.tool_calls {
|
||||
if !tool_calls.is_empty() {
|
||||
messages.push(Message {
|
||||
role: Role::Assistant,
|
||||
content: result.content.clone(),
|
||||
name: None,
|
||||
tool_calls: Some(tool_calls.clone()),
|
||||
tool_call_id: None,
|
||||
metadata: HashMap::new(),
|
||||
});
|
||||
|
||||
for tc in tool_calls {
|
||||
if let Some(loop_msg) = guard.check(&tc.name, &tc.arguments) {
|
||||
return Ok(AgentResult {
|
||||
content: format!(
|
||||
"Agent stopped: {}. Last response: {}",
|
||||
loop_msg, result.content
|
||||
),
|
||||
tool_results: all_tool_results,
|
||||
turns: turn,
|
||||
metadata: HashMap::new(),
|
||||
});
|
||||
}
|
||||
|
||||
let params: serde_json::Value =
|
||||
serde_json::from_str(&tc.arguments).unwrap_or(serde_json::json!({}));
|
||||
|
||||
let tool_result = match self.executor.execute(
|
||||
&tc.name,
|
||||
¶ms,
|
||||
Some("orchestrator"),
|
||||
None,
|
||||
) {
|
||||
Ok(r) => r,
|
||||
Err(e) => ToolResult::failure(&tc.name, e.to_string()),
|
||||
};
|
||||
|
||||
messages.push(Message {
|
||||
role: Role::Tool,
|
||||
content: tool_result.content.clone(),
|
||||
name: Some(tc.name.clone()),
|
||||
tool_calls: None,
|
||||
tool_call_id: Some(tc.id.clone()),
|
||||
metadata: HashMap::new(),
|
||||
});
|
||||
|
||||
all_tool_results.push(tool_result);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let content = AgentHelpers::strip_think_tags(&result.content);
|
||||
return Ok(AgentResult {
|
||||
content,
|
||||
tool_results: all_tool_results,
|
||||
turns: turn,
|
||||
metadata: HashMap::new(),
|
||||
});
|
||||
}
|
||||
Ok(AgentResult {
|
||||
content,
|
||||
tool_results: all_tool_results,
|
||||
turns: 1,
|
||||
metadata: HashMap::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
//! SimpleAgent — single-turn generation without tools.
|
||||
//!
|
||||
//! Wraps a rig-core `Agent<M>` for single-turn completion.
|
||||
|
||||
use crate::helpers::AgentHelpers;
|
||||
use crate::traits::Agent;
|
||||
use crate::traits::OjAgent;
|
||||
use crate::utils::strip_think_tags;
|
||||
use openjarvis_core::{AgentContext, AgentResult, OpenJarvisError};
|
||||
use openjarvis_engine::traits::InferenceEngine;
|
||||
use std::sync::Arc;
|
||||
use rig::agent::AgentBuilder;
|
||||
use rig::completion::request::{Chat, CompletionModel};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct SimpleAgent {
|
||||
helpers: AgentHelpers,
|
||||
/// Single-turn agent that delegates to rig-core's agent builder.
|
||||
pub struct SimpleAgent<M: CompletionModel> {
|
||||
agent: rig::agent::Agent<M>,
|
||||
}
|
||||
|
||||
impl SimpleAgent {
|
||||
pub fn new(
|
||||
engine: Arc<dyn InferenceEngine>,
|
||||
model: String,
|
||||
system_prompt: String,
|
||||
temperature: f64,
|
||||
max_tokens: i64,
|
||||
) -> Self {
|
||||
Self {
|
||||
helpers: AgentHelpers::new(engine, model, system_prompt, temperature, max_tokens),
|
||||
}
|
||||
impl<M: CompletionModel> SimpleAgent<M> {
|
||||
pub fn new(model: M, system_prompt: &str, temperature: f64) -> Self {
|
||||
let agent = AgentBuilder::new(model)
|
||||
.preamble(system_prompt)
|
||||
.temperature(temperature)
|
||||
.build();
|
||||
Self { agent }
|
||||
}
|
||||
}
|
||||
|
||||
impl Agent for SimpleAgent {
|
||||
#[async_trait::async_trait]
|
||||
impl<M: CompletionModel + 'static> OjAgent for SimpleAgent<M> {
|
||||
fn agent_id(&self) -> &str {
|
||||
"simple"
|
||||
}
|
||||
@@ -33,24 +34,46 @@ impl Agent for SimpleAgent {
|
||||
false
|
||||
}
|
||||
|
||||
fn run(
|
||||
async fn run(
|
||||
&self,
|
||||
input: &str,
|
||||
context: Option<&AgentContext>,
|
||||
) -> Result<AgentResult, OpenJarvisError> {
|
||||
let history = context
|
||||
.map(|c| c.conversation.messages.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
let messages = self.helpers.build_messages(input, history);
|
||||
let history: Vec<rig::completion::message::Message> = context
|
||||
.map(|ctx| {
|
||||
ctx.conversation
|
||||
.messages
|
||||
.iter()
|
||||
.filter_map(|m| match m.role {
|
||||
openjarvis_core::Role::User => {
|
||||
Some(rig::completion::message::Message::user(&m.content))
|
||||
}
|
||||
openjarvis_core::Role::Assistant => {
|
||||
Some(rig::completion::message::Message::assistant(&m.content))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let result = self.helpers.generate(&messages, None)?;
|
||||
let content = AgentHelpers::strip_think_tags(&result.content);
|
||||
let response = self
|
||||
.agent
|
||||
.chat(input, history)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
OpenJarvisError::Agent(openjarvis_core::error::AgentError::Execution(
|
||||
e.to_string(),
|
||||
))
|
||||
})?;
|
||||
|
||||
let content = strip_think_tags(&response);
|
||||
|
||||
Ok(AgentResult {
|
||||
content,
|
||||
tool_results: vec![],
|
||||
turns: 1,
|
||||
metadata: std::collections::HashMap::new(),
|
||||
metadata: HashMap::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
//! Agent trait — interface for all agent implementations.
|
||||
//! OjAgent trait — interface for all agent implementations.
|
||||
|
||||
use openjarvis_core::{AgentContext, AgentResult, OpenJarvisError};
|
||||
|
||||
pub trait Agent: Send + Sync {
|
||||
/// Core agent trait for all OpenJarvis agents.
|
||||
///
|
||||
/// Renamed from `Agent` to `OjAgent` to avoid collision with `rig::agent::Agent`.
|
||||
/// Async to support rig-core's async model.
|
||||
#[async_trait::async_trait]
|
||||
pub trait OjAgent: Send + Sync {
|
||||
fn agent_id(&self) -> &str;
|
||||
fn accepts_tools(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn run(
|
||||
async fn run(
|
||||
&self,
|
||||
input: &str,
|
||||
context: Option<&AgentContext>,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
//! Agent utilities — shared helper functions for all agent implementations.
|
||||
|
||||
use openjarvis_core::GenerateResult;
|
||||
use regex::Regex;
|
||||
|
||||
/// Strip `<think>...</think>` tags from model output.
|
||||
pub fn strip_think_tags(text: &str) -> String {
|
||||
let re = Regex::new(r"(?s)<think>.*?</think>").unwrap();
|
||||
re.replace_all(text, "").trim().to_string()
|
||||
}
|
||||
|
||||
/// Check if generation was cut off and needs continuation.
|
||||
pub fn check_continuation(result: &GenerateResult) -> bool {
|
||||
result.finish_reason == "length"
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_strip_think_tags() {
|
||||
let input = "Hello <think>internal reasoning</think> world";
|
||||
assert_eq!(strip_think_tags(input), "Hello world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_think_tags_multiline() {
|
||||
let input = "<think>\nstep 1\nstep 2\n</think>\nAnswer: 42";
|
||||
assert_eq!(strip_think_tags(input), "Answer: 42");
|
||||
}
|
||||
}
|
||||
@@ -188,6 +188,9 @@ pub enum AgentError {
|
||||
|
||||
#[error("Context overflow")]
|
||||
ContextOverflow,
|
||||
|
||||
#[error("Execution error: {0}")]
|
||||
Execution(String),
|
||||
}
|
||||
|
||||
/// Trace recording errors.
|
||||
|
||||
@@ -15,6 +15,8 @@ async-trait = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
rig-core = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["full", "test-util"] }
|
||||
|
||||
@@ -56,33 +56,43 @@ pub fn discover_engines(config: &JarvisConfig) -> Vec<EngineInfo> {
|
||||
found
|
||||
}
|
||||
|
||||
/// Get a configured engine instance by key.
|
||||
/// Get a configured engine instance by key (dynamic dispatch).
|
||||
pub fn get_engine(
|
||||
config: &JarvisConfig,
|
||||
engine_key: Option<&str>,
|
||||
) -> Result<Arc<dyn InferenceEngine>, OpenJarvisError> {
|
||||
Ok(Arc::new(get_engine_static(config, engine_key)?))
|
||||
}
|
||||
|
||||
/// Get a configured engine instance by key (static dispatch via `Engine` enum).
|
||||
pub fn get_engine_static(
|
||||
config: &JarvisConfig,
|
||||
engine_key: Option<&str>,
|
||||
) -> Result<crate::engine_enum::Engine, OpenJarvisError> {
|
||||
use crate::engine_enum::Engine;
|
||||
|
||||
let key = engine_key
|
||||
.map(String::from)
|
||||
.unwrap_or_else(|| config.engine.default.clone());
|
||||
|
||||
match key.as_str() {
|
||||
"ollama" => Ok(Arc::new(OllamaEngine::new(
|
||||
"ollama" => Ok(Engine::Ollama(OllamaEngine::new(
|
||||
&config.engine.ollama.host,
|
||||
120.0,
|
||||
))),
|
||||
"vllm" => Ok(Arc::new(OpenAICompatEngine::vllm(
|
||||
"vllm" => Ok(Engine::Vllm(OpenAICompatEngine::vllm(
|
||||
&config.engine.vllm.host,
|
||||
))),
|
||||
"sglang" => Ok(Arc::new(OpenAICompatEngine::sglang(
|
||||
"sglang" => Ok(Engine::Sglang(OpenAICompatEngine::sglang(
|
||||
&config.engine.sglang.host,
|
||||
))),
|
||||
"llamacpp" => Ok(Arc::new(OpenAICompatEngine::llamacpp(
|
||||
"llamacpp" => Ok(Engine::LlamaCpp(OpenAICompatEngine::llamacpp(
|
||||
&config.engine.llamacpp.host,
|
||||
))),
|
||||
"mlx" => Ok(Arc::new(OpenAICompatEngine::mlx(
|
||||
"mlx" => Ok(Engine::Mlx(OpenAICompatEngine::mlx(
|
||||
&config.engine.mlx.host,
|
||||
))),
|
||||
"lmstudio" => Ok(Arc::new(OpenAICompatEngine::lmstudio(
|
||||
"lmstudio" => Ok(Engine::LmStudio(OpenAICompatEngine::lmstudio(
|
||||
&config.engine.lmstudio.host,
|
||||
))),
|
||||
other => Err(OpenJarvisError::Engine(
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
//! Engine enum — static dispatch over all engine backends.
|
||||
//!
|
||||
//! Avoids `dyn InferenceEngine` for the hot path. Each variant holds a
|
||||
//! concrete engine so the compiler can inline and devirtualize.
|
||||
|
||||
use crate::ollama::OllamaEngine;
|
||||
use crate::openai_compat::OpenAICompatEngine;
|
||||
use crate::traits::{InferenceEngine, TokenStream};
|
||||
use openjarvis_core::error::OpenJarvisError;
|
||||
use openjarvis_core::{GenerateResult, Message};
|
||||
use serde_json::Value;
|
||||
|
||||
/// Closed enum of all supported inference engine backends.
|
||||
///
|
||||
/// Static dispatch at compile-time — no vtable overhead on the hot path.
|
||||
pub enum Engine {
|
||||
Ollama(OllamaEngine),
|
||||
Vllm(OpenAICompatEngine),
|
||||
Sglang(OpenAICompatEngine),
|
||||
LlamaCpp(OpenAICompatEngine),
|
||||
Mlx(OpenAICompatEngine),
|
||||
LmStudio(OpenAICompatEngine),
|
||||
}
|
||||
|
||||
macro_rules! delegate_engine {
|
||||
($self:expr, $method:ident $(, $arg:expr)*) => {
|
||||
match $self {
|
||||
Engine::Ollama(e) => e.$method($($arg),*),
|
||||
Engine::Vllm(e) => e.$method($($arg),*),
|
||||
Engine::Sglang(e) => e.$method($($arg),*),
|
||||
Engine::LlamaCpp(e) => e.$method($($arg),*),
|
||||
Engine::Mlx(e) => e.$method($($arg),*),
|
||||
Engine::LmStudio(e) => e.$method($($arg),*),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl InferenceEngine for Engine {
|
||||
fn engine_id(&self) -> &str {
|
||||
delegate_engine!(self, engine_id)
|
||||
}
|
||||
|
||||
fn generate(
|
||||
&self,
|
||||
messages: &[Message],
|
||||
model: &str,
|
||||
temperature: f64,
|
||||
max_tokens: i64,
|
||||
extra: Option<&Value>,
|
||||
) -> Result<GenerateResult, OpenJarvisError> {
|
||||
delegate_engine!(self, generate, messages, model, temperature, max_tokens, extra)
|
||||
}
|
||||
|
||||
async fn stream(
|
||||
&self,
|
||||
messages: &[Message],
|
||||
model: &str,
|
||||
temperature: f64,
|
||||
max_tokens: i64,
|
||||
extra: Option<&Value>,
|
||||
) -> Result<TokenStream, OpenJarvisError> {
|
||||
match self {
|
||||
Engine::Ollama(e) => e.stream(messages, model, temperature, max_tokens, extra).await,
|
||||
Engine::Vllm(e) => e.stream(messages, model, temperature, max_tokens, extra).await,
|
||||
Engine::Sglang(e) => e.stream(messages, model, temperature, max_tokens, extra).await,
|
||||
Engine::LlamaCpp(e) => e.stream(messages, model, temperature, max_tokens, extra).await,
|
||||
Engine::Mlx(e) => e.stream(messages, model, temperature, max_tokens, extra).await,
|
||||
Engine::LmStudio(e) => e.stream(messages, model, temperature, max_tokens, extra).await,
|
||||
}
|
||||
}
|
||||
|
||||
fn list_models(&self) -> Result<Vec<String>, OpenJarvisError> {
|
||||
delegate_engine!(self, list_models)
|
||||
}
|
||||
|
||||
fn health(&self) -> bool {
|
||||
delegate_engine!(self, health)
|
||||
}
|
||||
|
||||
fn close(&self) {
|
||||
delegate_engine!(self, close)
|
||||
}
|
||||
|
||||
fn prepare(&self, model: &str) {
|
||||
delegate_engine!(self, prepare, model)
|
||||
}
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
/// Convenience: identify the engine variant key (e.g. "ollama", "vllm").
|
||||
pub fn variant_key(&self) -> &str {
|
||||
match self {
|
||||
Engine::Ollama(_) => "ollama",
|
||||
Engine::Vllm(_) => "vllm",
|
||||
Engine::Sglang(_) => "sglang",
|
||||
Engine::LlamaCpp(_) => "llamacpp",
|
||||
Engine::Mlx(_) => "mlx",
|
||||
Engine::LmStudio(_) => "lmstudio",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_engine_variant_key() {
|
||||
let e = Engine::Ollama(OllamaEngine::with_defaults());
|
||||
assert_eq!(e.variant_key(), "ollama");
|
||||
assert_eq!(e.engine_id(), "ollama");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_vllm_variant() {
|
||||
let e = Engine::Vllm(OpenAICompatEngine::vllm("http://localhost:8000"));
|
||||
assert_eq!(e.variant_key(), "vllm");
|
||||
assert_eq!(e.engine_id(), "vllm");
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,14 @@
|
||||
//! cloud providers, OpenAI-compatible servers).
|
||||
|
||||
pub mod discovery;
|
||||
pub mod engine_enum;
|
||||
pub mod ollama;
|
||||
pub mod openai_compat;
|
||||
pub mod rig_adapter;
|
||||
pub mod traits;
|
||||
|
||||
pub use discovery::{discover_engines, get_engine};
|
||||
pub use discovery::{discover_engines, get_engine, get_engine_static};
|
||||
pub use engine_enum::Engine;
|
||||
pub use ollama::OllamaEngine;
|
||||
pub use openai_compat::OpenAICompatEngine;
|
||||
pub use traits::{InferenceEngine, messages_to_dicts};
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
//! Rig-core model adapter — bridges `InferenceEngine` into rig's `CompletionModel`.
|
||||
|
||||
use crate::traits::InferenceEngine;
|
||||
use openjarvis_core::{GenerateResult, Message};
|
||||
use rig::completion::message::Message as RigMessage;
|
||||
use rig::completion::request::{
|
||||
CompletionError, CompletionRequest, CompletionResponse, ToolDefinition,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Raw response wrapper for our engine results.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct OjRawResponse {
|
||||
pub content: String,
|
||||
pub model: String,
|
||||
pub finish_reason: String,
|
||||
pub prompt_tokens: i64,
|
||||
pub completion_tokens: i64,
|
||||
}
|
||||
|
||||
impl rig::completion::request::GetTokenUsage for OjRawResponse {
|
||||
fn token_usage(&self) -> Option<rig::completion::Usage> {
|
||||
Some(rig::completion::Usage {
|
||||
input_tokens: self.prompt_tokens as u64,
|
||||
output_tokens: self.completion_tokens as u64,
|
||||
total_tokens: (self.prompt_tokens + self.completion_tokens) as u64,
|
||||
cached_input_tokens: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Bridges any `InferenceEngine` implementation into rig-core's `CompletionModel`.
|
||||
///
|
||||
/// Uses `tokio::task::spawn_blocking` to bridge the sync `generate()` method
|
||||
/// into rig's async completion interface.
|
||||
pub struct RigModelAdapter<E: InferenceEngine> {
|
||||
engine: Arc<E>,
|
||||
model_id: String,
|
||||
}
|
||||
|
||||
impl<E: InferenceEngine> RigModelAdapter<E> {
|
||||
pub fn new(engine: Arc<E>, model_id: String) -> Self {
|
||||
Self { engine, model_id }
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: InferenceEngine> Clone for RigModelAdapter<E> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
engine: Arc::clone(&self.engine),
|
||||
model_id: self.model_id.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert rig-core messages to openjarvis Messages.
|
||||
fn rig_request_to_oj_messages(request: &CompletionRequest) -> Vec<Message> {
|
||||
let mut messages = Vec::new();
|
||||
|
||||
// Add preamble as system message
|
||||
if let Some(ref preamble) = request.preamble {
|
||||
messages.push(Message::system(preamble));
|
||||
}
|
||||
|
||||
// Add chat history
|
||||
for msg in request.chat_history.iter() {
|
||||
match msg {
|
||||
RigMessage::User { content } => {
|
||||
let text = content
|
||||
.iter()
|
||||
.filter_map(|c| {
|
||||
if let rig::completion::message::UserContent::Text(t) = c {
|
||||
Some(t.text.as_str())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
messages.push(Message::user(&text));
|
||||
}
|
||||
RigMessage::Assistant { content, .. } => {
|
||||
let text = content
|
||||
.iter()
|
||||
.filter_map(|c| {
|
||||
if let rig::completion::message::AssistantContent::Text(t) = c {
|
||||
Some(t.text.as_str())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
messages.push(Message::assistant(&text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add document context
|
||||
if !request.documents.is_empty() {
|
||||
let doc_context: String = request
|
||||
.documents
|
||||
.iter()
|
||||
.map(|d| format!("[{}]\n{}", d.id, d.text))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n");
|
||||
messages.push(Message::system(&format!(
|
||||
"Relevant context:\n{}",
|
||||
doc_context
|
||||
)));
|
||||
}
|
||||
|
||||
messages
|
||||
}
|
||||
|
||||
/// Build the `extra` JSON Value for tool definitions.
|
||||
fn tools_to_extra(tools: &[ToolDefinition]) -> Option<Value> {
|
||||
if tools.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let tool_specs: Vec<Value> = tools
|
||||
.iter()
|
||||
.map(|t| {
|
||||
serde_json::json!({
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": t.name,
|
||||
"description": t.description,
|
||||
"parameters": t.parameters,
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
Some(serde_json::json!({ "tools": tool_specs }))
|
||||
}
|
||||
|
||||
fn make_usage(result: &GenerateResult) -> rig::completion::Usage {
|
||||
rig::completion::Usage {
|
||||
input_tokens: result.usage.prompt_tokens as u64,
|
||||
output_tokens: result.usage.completion_tokens as u64,
|
||||
total_tokens: result.usage.total_tokens as u64,
|
||||
cached_input_tokens: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_raw(result: &GenerateResult) -> OjRawResponse {
|
||||
OjRawResponse {
|
||||
content: result.content.clone(),
|
||||
model: result.model.clone(),
|
||||
finish_reason: result.finish_reason.clone(),
|
||||
prompt_tokens: result.usage.prompt_tokens,
|
||||
completion_tokens: result.usage.completion_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: InferenceEngine + 'static> rig::completion::request::CompletionModel
|
||||
for RigModelAdapter<E>
|
||||
{
|
||||
type Response = OjRawResponse;
|
||||
type StreamingResponse = OjRawResponse;
|
||||
type Client = ();
|
||||
|
||||
fn make(_client: &Self::Client, _model: impl Into<String>) -> Self {
|
||||
unimplemented!(
|
||||
"Use RigModelAdapter::new() directly instead of CompletionModel::make()"
|
||||
);
|
||||
}
|
||||
|
||||
fn completion(
|
||||
&self,
|
||||
request: CompletionRequest,
|
||||
) -> impl std::future::Future<
|
||||
Output = Result<CompletionResponse<Self::Response>, CompletionError>,
|
||||
> + Send {
|
||||
let engine = Arc::clone(&self.engine);
|
||||
let model_id = self.model_id.clone();
|
||||
|
||||
async move {
|
||||
let messages = rig_request_to_oj_messages(&request);
|
||||
let temperature = request.temperature.unwrap_or(0.7);
|
||||
let max_tokens = request.max_tokens.unwrap_or(2048) as i64;
|
||||
let extra = tools_to_extra(&request.tools);
|
||||
|
||||
let result: Result<GenerateResult, _> =
|
||||
tokio::task::spawn_blocking(move || {
|
||||
engine.generate(
|
||||
&messages,
|
||||
&model_id,
|
||||
temperature,
|
||||
max_tokens,
|
||||
extra.as_ref(),
|
||||
)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| CompletionError::ProviderError(e.to_string()))?;
|
||||
|
||||
let result =
|
||||
result.map_err(|e| CompletionError::ProviderError(e.to_string()))?;
|
||||
|
||||
let raw = make_raw(&result);
|
||||
let usage = make_usage(&result);
|
||||
|
||||
let choice = rig::one_or_many::OneOrMany::one(
|
||||
rig::completion::message::AssistantContent::text(&result.content),
|
||||
);
|
||||
|
||||
Ok(CompletionResponse {
|
||||
choice,
|
||||
usage,
|
||||
raw_response: raw,
|
||||
message_id: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn stream(
|
||||
&self,
|
||||
_request: CompletionRequest,
|
||||
) -> impl std::future::Future<
|
||||
Output = Result<
|
||||
rig::streaming::StreamingCompletionResponse<Self::StreamingResponse>,
|
||||
CompletionError,
|
||||
>,
|
||||
> + Send {
|
||||
async move {
|
||||
// Our engines use blocking HTTP clients. Streaming is not supported
|
||||
// through the rig adapter — callers should use `completion()` instead.
|
||||
Err(CompletionError::ProviderError(
|
||||
"Streaming not supported through RigModelAdapter; use completion() instead".into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use openjarvis_core::Role;
|
||||
|
||||
#[test]
|
||||
fn test_rig_request_to_oj_messages_basic() {
|
||||
let request = CompletionRequest {
|
||||
model: None,
|
||||
preamble: Some("You are helpful".into()),
|
||||
chat_history: rig::one_or_many::OneOrMany::one(RigMessage::user("Hello")),
|
||||
documents: vec![],
|
||||
tools: vec![],
|
||||
temperature: None,
|
||||
max_tokens: None,
|
||||
tool_choice: None,
|
||||
additional_params: None,
|
||||
output_schema: None,
|
||||
};
|
||||
|
||||
let msgs = rig_request_to_oj_messages(&request);
|
||||
assert_eq!(msgs.len(), 2);
|
||||
assert_eq!(msgs[0].role, Role::System);
|
||||
assert_eq!(msgs[1].role, Role::User);
|
||||
assert_eq!(msgs[1].content, "Hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tools_to_extra() {
|
||||
let tools = vec![ToolDefinition {
|
||||
name: "calculator".into(),
|
||||
description: "Compute math".into(),
|
||||
parameters: serde_json::json!({"type": "object"}),
|
||||
}];
|
||||
let extra = tools_to_extra(&tools);
|
||||
assert!(extra.is_some());
|
||||
let v = extra.unwrap();
|
||||
assert!(v["tools"].is_array());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tools_to_extra_empty() {
|
||||
assert!(tools_to_extra(&[]).is_none());
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,47 @@ pub trait InferenceEngine: Send + Sync {
|
||||
fn prepare(&self, _model: &str) {}
|
||||
}
|
||||
|
||||
/// Blanket impl: `Arc<dyn InferenceEngine>` delegates to the inner engine.
|
||||
/// This enables generic wrappers like `GuardrailsEngine<Arc<dyn InferenceEngine>>`.
|
||||
#[async_trait::async_trait]
|
||||
impl InferenceEngine for std::sync::Arc<dyn InferenceEngine> {
|
||||
fn engine_id(&self) -> &str {
|
||||
(**self).engine_id()
|
||||
}
|
||||
fn generate(
|
||||
&self,
|
||||
messages: &[Message],
|
||||
model: &str,
|
||||
temperature: f64,
|
||||
max_tokens: i64,
|
||||
extra: Option<&Value>,
|
||||
) -> Result<GenerateResult, openjarvis_core::OpenJarvisError> {
|
||||
(**self).generate(messages, model, temperature, max_tokens, extra)
|
||||
}
|
||||
async fn stream(
|
||||
&self,
|
||||
messages: &[Message],
|
||||
model: &str,
|
||||
temperature: f64,
|
||||
max_tokens: i64,
|
||||
extra: Option<&Value>,
|
||||
) -> Result<TokenStream, openjarvis_core::OpenJarvisError> {
|
||||
(**self).stream(messages, model, temperature, max_tokens, extra).await
|
||||
}
|
||||
fn list_models(&self) -> Result<Vec<String>, openjarvis_core::OpenJarvisError> {
|
||||
(**self).list_models()
|
||||
}
|
||||
fn health(&self) -> bool {
|
||||
(**self).health()
|
||||
}
|
||||
fn close(&self) {
|
||||
(**self).close()
|
||||
}
|
||||
fn prepare(&self, model: &str) {
|
||||
(**self).prepare(model)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert `Message` structs to OpenAI-compatible JSON dicts.
|
||||
pub fn messages_to_dicts(messages: &[Message]) -> Vec<Value> {
|
||||
messages
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
pub mod bandit;
|
||||
pub mod grpo;
|
||||
pub mod heuristic;
|
||||
pub mod router_enum;
|
||||
pub mod traits;
|
||||
|
||||
pub use bandit::BanditRouterPolicy;
|
||||
pub use grpo::GRPORouterPolicy;
|
||||
pub use heuristic::HeuristicRouter;
|
||||
pub use router_enum::RouterPolicyEnum;
|
||||
pub use traits::{LearningPolicy, RouterPolicy};
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
//! RouterPolicyEnum — static dispatch over router policy implementations.
|
||||
|
||||
use crate::bandit::BanditRouterPolicy;
|
||||
use crate::grpo::GRPORouterPolicy;
|
||||
use crate::heuristic::HeuristicRouter;
|
||||
use crate::traits::RouterPolicy;
|
||||
use openjarvis_core::RoutingContext;
|
||||
|
||||
/// Closed enum of all supported router policies.
|
||||
pub enum RouterPolicyEnum {
|
||||
Heuristic(HeuristicRouter),
|
||||
Bandit(BanditRouterPolicy),
|
||||
Grpo(GRPORouterPolicy),
|
||||
}
|
||||
|
||||
impl RouterPolicy for RouterPolicyEnum {
|
||||
fn select_model(&self, context: &RoutingContext) -> String {
|
||||
match self {
|
||||
RouterPolicyEnum::Heuristic(r) => r.select_model(context),
|
||||
RouterPolicyEnum::Bandit(r) => r.select_model(context),
|
||||
RouterPolicyEnum::Grpo(r) => r.select_model(context),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RouterPolicyEnum {
|
||||
/// Convenience: identify the policy variant key.
|
||||
pub fn variant_key(&self) -> &str {
|
||||
match self {
|
||||
RouterPolicyEnum::Heuristic(_) => "heuristic",
|
||||
RouterPolicyEnum::Bandit(_) => "bandit",
|
||||
RouterPolicyEnum::Grpo(_) => "grpo",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,3 +20,6 @@ openjarvis-mcp = { path = "../openjarvis-mcp" }
|
||||
pyo3 = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
//! PyO3 bindings for agent types.
|
||||
//!
|
||||
//! At the Python boundary, agents use `Box<dyn OjAgent>` for type erasure
|
||||
//! since Python can't handle Rust generics. The shared tokio Runtime
|
||||
//! bridges async→sync.
|
||||
|
||||
use crate::core::PyAgentResult;
|
||||
use crate::RUNTIME;
|
||||
use openjarvis_agents::OjAgent;
|
||||
use pyo3::prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Python wrapper for SimpleAgent (type-erased via Box<dyn OjAgent>).
|
||||
#[pyclass(name = "SimpleAgent")]
|
||||
pub struct PySimpleAgent {
|
||||
inner: Box<dyn OjAgent>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PySimpleAgent {
|
||||
/// Create a SimpleAgent backed by an Engine enum.
|
||||
#[new]
|
||||
#[pyo3(signature = (engine_key="ollama", host="http://localhost:11434", model="qwen3:8b", system_prompt="You are a helpful assistant.", temperature=0.7))]
|
||||
fn new(
|
||||
engine_key: &str,
|
||||
host: &str,
|
||||
model: &str,
|
||||
system_prompt: &str,
|
||||
temperature: f64,
|
||||
) -> PyResult<Self> {
|
||||
let config = openjarvis_core::JarvisConfig::default();
|
||||
let engine = openjarvis_engine::get_engine_static(&config, Some(engine_key))
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
let adapter = openjarvis_engine::rig_adapter::RigModelAdapter::new(
|
||||
Arc::new(engine),
|
||||
model.to_string(),
|
||||
);
|
||||
let agent = openjarvis_agents::SimpleAgent::new(adapter, system_prompt, temperature);
|
||||
Ok(Self {
|
||||
inner: Box::new(agent),
|
||||
})
|
||||
}
|
||||
|
||||
fn agent_id(&self) -> &str {
|
||||
self.inner.agent_id()
|
||||
}
|
||||
|
||||
fn accepts_tools(&self) -> bool {
|
||||
self.inner.accepts_tools()
|
||||
}
|
||||
|
||||
fn run(&self, input: &str) -> PyResult<PyAgentResult> {
|
||||
let result = RUNTIME
|
||||
.block_on(self.inner.run(input, None))
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(PyAgentResult {
|
||||
content: result.content,
|
||||
turns: result.turns,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Python wrapper for OrchestratorAgent.
|
||||
#[pyclass(name = "OrchestratorAgent")]
|
||||
pub struct PyOrchestratorAgent {
|
||||
inner: Box<dyn OjAgent>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyOrchestratorAgent {
|
||||
#[new]
|
||||
#[pyo3(signature = (engine_key="ollama", host="http://localhost:11434", model="qwen3:8b", system_prompt="You are a helpful orchestrator agent.", max_turns=10, temperature=0.7))]
|
||||
fn new(
|
||||
engine_key: &str,
|
||||
host: &str,
|
||||
model: &str,
|
||||
system_prompt: &str,
|
||||
max_turns: usize,
|
||||
temperature: f64,
|
||||
) -> PyResult<Self> {
|
||||
let config = openjarvis_core::JarvisConfig::default();
|
||||
let engine = openjarvis_engine::get_engine_static(&config, Some(engine_key))
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
let adapter = openjarvis_engine::rig_adapter::RigModelAdapter::new(
|
||||
Arc::new(engine),
|
||||
model.to_string(),
|
||||
);
|
||||
let executor = Arc::new(openjarvis_tools::ToolExecutor::new(None, None));
|
||||
let agent = openjarvis_agents::OrchestratorAgent::new(
|
||||
adapter,
|
||||
system_prompt,
|
||||
executor,
|
||||
max_turns,
|
||||
temperature,
|
||||
);
|
||||
Ok(Self {
|
||||
inner: Box::new(agent),
|
||||
})
|
||||
}
|
||||
|
||||
fn agent_id(&self) -> &str {
|
||||
self.inner.agent_id()
|
||||
}
|
||||
|
||||
fn accepts_tools(&self) -> bool {
|
||||
self.inner.accepts_tools()
|
||||
}
|
||||
|
||||
fn run(&self, input: &str) -> PyResult<PyAgentResult> {
|
||||
let result = RUNTIME
|
||||
.block_on(self.inner.run(input, None))
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(PyAgentResult {
|
||||
content: result.content,
|
||||
turns: result.turns,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Python wrapper for NativeReActAgent.
|
||||
#[pyclass(name = "NativeReActAgent")]
|
||||
pub struct PyNativeReActAgent {
|
||||
inner: Box<dyn OjAgent>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyNativeReActAgent {
|
||||
#[new]
|
||||
#[pyo3(signature = (engine_key="ollama", host="http://localhost:11434", model="qwen3:8b", max_turns=10, temperature=0.7))]
|
||||
fn new(
|
||||
engine_key: &str,
|
||||
host: &str,
|
||||
model: &str,
|
||||
max_turns: usize,
|
||||
temperature: f64,
|
||||
) -> PyResult<Self> {
|
||||
let config = openjarvis_core::JarvisConfig::default();
|
||||
let engine = openjarvis_engine::get_engine_static(&config, Some(engine_key))
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
let adapter = openjarvis_engine::rig_adapter::RigModelAdapter::new(
|
||||
Arc::new(engine),
|
||||
model.to_string(),
|
||||
);
|
||||
let executor = Arc::new(openjarvis_tools::ToolExecutor::new(None, None));
|
||||
let agent = openjarvis_agents::NativeReActAgent::new(
|
||||
adapter,
|
||||
executor,
|
||||
max_turns,
|
||||
temperature,
|
||||
);
|
||||
Ok(Self {
|
||||
inner: Box::new(agent),
|
||||
})
|
||||
}
|
||||
|
||||
fn agent_id(&self) -> &str {
|
||||
self.inner.agent_id()
|
||||
}
|
||||
|
||||
fn accepts_tools(&self) -> bool {
|
||||
self.inner.accepts_tools()
|
||||
}
|
||||
|
||||
fn run(&self, input: &str) -> PyResult<PyAgentResult> {
|
||||
let result = RUNTIME
|
||||
.block_on(self.inner.run(input, None))
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(PyAgentResult {
|
||||
content: result.content,
|
||||
turns: result.turns,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Python wrapper for LoopGuard.
|
||||
#[pyclass(name = "LoopGuard")]
|
||||
pub struct PyLoopGuard {
|
||||
inner: openjarvis_agents::LoopGuard,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyLoopGuard {
|
||||
#[new]
|
||||
#[pyo3(signature = (max_identical=50, max_ping_pong=4, poll_budget=100))]
|
||||
fn new(max_identical: usize, max_ping_pong: usize, poll_budget: usize) -> Self {
|
||||
Self {
|
||||
inner: openjarvis_agents::LoopGuard::new(max_identical, max_ping_pong, poll_budget),
|
||||
}
|
||||
}
|
||||
|
||||
fn check(&mut self, tool_name: &str, arguments: &str) -> Option<String> {
|
||||
self.inner.check(tool_name, arguments)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
//! PyO3 bindings for core types.
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[pyclass(name = "Message")]
|
||||
#[derive(Clone)]
|
||||
pub struct PyMessage {
|
||||
#[pyo3(get, set)]
|
||||
pub role: String,
|
||||
#[pyo3(get, set)]
|
||||
pub content: String,
|
||||
#[pyo3(get, set)]
|
||||
pub name: Option<String>,
|
||||
#[pyo3(get, set)]
|
||||
pub tool_call_id: Option<String>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyMessage {
|
||||
#[new]
|
||||
fn new(role: String, content: String) -> Self {
|
||||
Self {
|
||||
role,
|
||||
content,
|
||||
name: None,
|
||||
tool_call_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!("Message(role='{}', content='{}')", self.role, &self.content[..self.content.len().min(50)])
|
||||
}
|
||||
}
|
||||
|
||||
impl PyMessage {
|
||||
pub fn to_core(&self) -> openjarvis_core::Message {
|
||||
let role = match self.role.as_str() {
|
||||
"system" => openjarvis_core::Role::System,
|
||||
"assistant" => openjarvis_core::Role::Assistant,
|
||||
"tool" => openjarvis_core::Role::Tool,
|
||||
_ => openjarvis_core::Role::User,
|
||||
};
|
||||
openjarvis_core::Message {
|
||||
role,
|
||||
content: self.content.clone(),
|
||||
name: self.name.clone(),
|
||||
tool_calls: None,
|
||||
tool_call_id: self.tool_call_id.clone(),
|
||||
metadata: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "ToolResult")]
|
||||
#[derive(Clone)]
|
||||
pub struct PyToolResult {
|
||||
#[pyo3(get)]
|
||||
pub tool_name: String,
|
||||
#[pyo3(get)]
|
||||
pub content: String,
|
||||
#[pyo3(get)]
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyToolResult {
|
||||
#[new]
|
||||
fn new(tool_name: String, content: String, success: bool) -> Self {
|
||||
Self { tool_name, content, success }
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!("ToolResult(tool='{}', success={})", self.tool_name, self.success)
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "ToolCall")]
|
||||
#[derive(Clone)]
|
||||
pub struct PyToolCall {
|
||||
#[pyo3(get, set)]
|
||||
pub id: String,
|
||||
#[pyo3(get, set)]
|
||||
pub name: String,
|
||||
#[pyo3(get, set)]
|
||||
pub arguments: String,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyToolCall {
|
||||
#[new]
|
||||
fn new(id: String, name: String, arguments: String) -> Self {
|
||||
Self { id, name, arguments }
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "Config")]
|
||||
pub struct PyConfig {
|
||||
pub inner: openjarvis_core::JarvisConfig,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyConfig {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: openjarvis_core::JarvisConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!(
|
||||
"Config(engine={}, model={})",
|
||||
self.inner.engine.default, self.inner.intelligence.default_model
|
||||
)
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn engine_default(&self) -> String {
|
||||
self.inner.engine.default.clone()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn model_default(&self) -> String {
|
||||
self.inner.intelligence.default_model.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "EventBus")]
|
||||
pub struct PyEventBus {
|
||||
pub inner: std::sync::Arc<openjarvis_core::EventBus>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyEventBus {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: std::sync::Arc::new(openjarvis_core::EventBus::new(true)),
|
||||
}
|
||||
}
|
||||
|
||||
fn history_len(&self) -> usize {
|
||||
self.inner.history().len()
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "ModelSpec")]
|
||||
#[derive(Clone)]
|
||||
pub struct PyModelSpec {
|
||||
#[pyo3(get, set)]
|
||||
pub name: String,
|
||||
#[pyo3(get, set)]
|
||||
pub params_b: f64,
|
||||
#[pyo3(get, set)]
|
||||
pub context_length: usize,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyModelSpec {
|
||||
#[new]
|
||||
fn new(name: String, params_b: f64, context_length: usize) -> Self {
|
||||
Self { name, params_b, context_length }
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "RoutingContext")]
|
||||
#[derive(Clone)]
|
||||
pub struct PyRoutingContext {
|
||||
#[pyo3(get, set)]
|
||||
pub query: String,
|
||||
#[pyo3(get, set)]
|
||||
pub query_class: String,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyRoutingContext {
|
||||
#[new]
|
||||
fn new(query: String) -> Self {
|
||||
Self { query, query_class: "general".into() }
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "AgentContext")]
|
||||
pub struct PyAgentContext {
|
||||
#[pyo3(get, set)]
|
||||
pub session_id: String,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyAgentContext {
|
||||
#[new]
|
||||
fn new(session_id: String) -> Self {
|
||||
Self { session_id }
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "AgentResult")]
|
||||
#[derive(Clone)]
|
||||
pub struct PyAgentResult {
|
||||
#[pyo3(get)]
|
||||
pub content: String,
|
||||
#[pyo3(get)]
|
||||
pub turns: usize,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyAgentResult {
|
||||
fn __repr__(&self) -> String {
|
||||
format!("AgentResult(turns={}, content='{}')", self.turns, &self.content[..self.content.len().min(50)])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
//! PyO3 bindings for engine types.
|
||||
|
||||
use crate::core::PyMessage;
|
||||
use openjarvis_engine::InferenceEngine;
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// Wraps the Engine enum (static dispatch internally, opaque to Python).
|
||||
#[pyclass(name = "Engine")]
|
||||
pub struct PyEngine {
|
||||
pub inner: openjarvis_engine::Engine,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyEngine {
|
||||
/// Create an engine by key (e.g. "ollama", "vllm", "sglang", "llamacpp", "mlx", "lmstudio").
|
||||
#[new]
|
||||
#[pyo3(signature = (engine_key="ollama", host=None))]
|
||||
fn new(engine_key: &str, host: Option<&str>) -> PyResult<Self> {
|
||||
let engine = match engine_key {
|
||||
"ollama" => openjarvis_engine::Engine::Ollama(
|
||||
openjarvis_engine::OllamaEngine::new(
|
||||
host.unwrap_or("http://localhost:11434"),
|
||||
120.0,
|
||||
),
|
||||
),
|
||||
"vllm" => openjarvis_engine::Engine::Vllm(
|
||||
openjarvis_engine::OpenAICompatEngine::vllm(
|
||||
host.unwrap_or("http://localhost:8000"),
|
||||
),
|
||||
),
|
||||
"sglang" => openjarvis_engine::Engine::Sglang(
|
||||
openjarvis_engine::OpenAICompatEngine::sglang(
|
||||
host.unwrap_or("http://localhost:30000"),
|
||||
),
|
||||
),
|
||||
"llamacpp" => openjarvis_engine::Engine::LlamaCpp(
|
||||
openjarvis_engine::OpenAICompatEngine::llamacpp(
|
||||
host.unwrap_or("http://localhost:8080"),
|
||||
),
|
||||
),
|
||||
"mlx" => openjarvis_engine::Engine::Mlx(
|
||||
openjarvis_engine::OpenAICompatEngine::mlx(
|
||||
host.unwrap_or("http://localhost:8080"),
|
||||
),
|
||||
),
|
||||
"lmstudio" => openjarvis_engine::Engine::LmStudio(
|
||||
openjarvis_engine::OpenAICompatEngine::lmstudio(
|
||||
host.unwrap_or("http://localhost:1234"),
|
||||
),
|
||||
),
|
||||
other => {
|
||||
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
|
||||
format!("Unknown engine: {}", other),
|
||||
));
|
||||
}
|
||||
};
|
||||
Ok(Self { inner: engine })
|
||||
}
|
||||
|
||||
fn engine_id(&self) -> &str {
|
||||
self.inner.engine_id()
|
||||
}
|
||||
|
||||
fn variant_key(&self) -> &str {
|
||||
self.inner.variant_key()
|
||||
}
|
||||
|
||||
fn health(&self) -> bool {
|
||||
self.inner.health()
|
||||
}
|
||||
|
||||
fn list_models(&self) -> PyResult<Vec<String>> {
|
||||
self.inner
|
||||
.list_models()
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
|
||||
}
|
||||
|
||||
#[pyo3(signature = (messages, model, temperature=0.7, max_tokens=1024))]
|
||||
fn generate(
|
||||
&self,
|
||||
messages: Vec<PyMessage>,
|
||||
model: &str,
|
||||
temperature: f64,
|
||||
max_tokens: i64,
|
||||
) -> PyResult<String> {
|
||||
let core_msgs: Vec<openjarvis_core::Message> =
|
||||
messages.iter().map(|m| m.to_core()).collect();
|
||||
let result = self
|
||||
.inner
|
||||
.generate(&core_msgs, model, temperature, max_tokens, None)
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(serde_json::to_string(&result).unwrap_or_default())
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!("Engine({})", self.inner.variant_key())
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience alias for backward compatibility.
|
||||
#[pyclass(name = "OllamaEngine")]
|
||||
pub struct PyOllamaEngine {
|
||||
inner: openjarvis_engine::OllamaEngine,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyOllamaEngine {
|
||||
#[new]
|
||||
#[pyo3(signature = (host="http://localhost:11434", timeout=120.0))]
|
||||
fn new(host: &str, timeout: f64) -> Self {
|
||||
Self {
|
||||
inner: openjarvis_engine::OllamaEngine::new(host, timeout),
|
||||
}
|
||||
}
|
||||
|
||||
fn engine_id(&self) -> &str {
|
||||
self.inner.engine_id()
|
||||
}
|
||||
|
||||
fn health(&self) -> bool {
|
||||
self.inner.health()
|
||||
}
|
||||
|
||||
fn list_models(&self) -> PyResult<Vec<String>> {
|
||||
self.inner
|
||||
.list_models()
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
|
||||
}
|
||||
|
||||
#[pyo3(signature = (messages, model, temperature=0.7, max_tokens=1024))]
|
||||
fn generate(
|
||||
&self,
|
||||
messages: Vec<PyMessage>,
|
||||
model: &str,
|
||||
temperature: f64,
|
||||
max_tokens: i64,
|
||||
) -> PyResult<String> {
|
||||
let core_msgs: Vec<openjarvis_core::Message> =
|
||||
messages.iter().map(|m| m.to_core()).collect();
|
||||
let result = self
|
||||
.inner
|
||||
.generate(&core_msgs, model, temperature, max_tokens, None)
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(serde_json::to_string(&result).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
//! PyO3 bindings for learning/router policy types.
|
||||
|
||||
use openjarvis_learning::RouterPolicy;
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[pyclass(name = "HeuristicRouter")]
|
||||
pub struct PyHeuristicRouter {
|
||||
inner: openjarvis_learning::HeuristicRouter,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyHeuristicRouter {
|
||||
#[new]
|
||||
#[pyo3(signature = (default_model="qwen3:8b", code_model=None, math_model=None, fast_model=None))]
|
||||
fn new(
|
||||
default_model: &str,
|
||||
code_model: Option<String>,
|
||||
math_model: Option<String>,
|
||||
fast_model: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: openjarvis_learning::HeuristicRouter::new(
|
||||
default_model.to_string(),
|
||||
code_model,
|
||||
math_model,
|
||||
fast_model,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn select_model(&self, query: &str, has_code: bool, has_math: bool) -> String {
|
||||
let ctx = openjarvis_core::RoutingContext {
|
||||
query: query.to_string(),
|
||||
query_length: query.len(),
|
||||
has_code,
|
||||
has_math,
|
||||
..Default::default()
|
||||
};
|
||||
self.inner.select_model(&ctx)
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "BanditRouterPolicy")]
|
||||
pub struct PyBanditRouterPolicy {
|
||||
inner: openjarvis_learning::BanditRouterPolicy,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyBanditRouterPolicy {
|
||||
#[new]
|
||||
#[pyo3(signature = (models, strategy="thompson"))]
|
||||
fn new(models: Vec<String>, strategy: &str) -> Self {
|
||||
let strat = match strategy {
|
||||
"ucb1" | "UCB1" => openjarvis_learning::bandit::BanditStrategy::UCB1,
|
||||
_ => openjarvis_learning::bandit::BanditStrategy::ThompsonSampling,
|
||||
};
|
||||
Self {
|
||||
inner: openjarvis_learning::BanditRouterPolicy::new(models, strat),
|
||||
}
|
||||
}
|
||||
|
||||
fn select_model(&self) -> String {
|
||||
let ctx = openjarvis_core::RoutingContext::default();
|
||||
self.inner.select_model(&ctx)
|
||||
}
|
||||
|
||||
fn update(&self, model: &str, reward: f64) {
|
||||
self.inner.update(model, reward);
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "GRPORouterPolicy")]
|
||||
pub struct PyGRPORouterPolicy {
|
||||
inner: openjarvis_learning::GRPORouterPolicy,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyGRPORouterPolicy {
|
||||
#[new]
|
||||
#[pyo3(signature = (models, temperature=1.0))]
|
||||
fn new(models: Vec<String>, temperature: f64) -> Self {
|
||||
Self {
|
||||
inner: openjarvis_learning::GRPORouterPolicy::new(models, temperature),
|
||||
}
|
||||
}
|
||||
|
||||
fn select_model(&self) -> String {
|
||||
let ctx = openjarvis_core::RoutingContext::default();
|
||||
self.inner.select_model(&ctx)
|
||||
}
|
||||
|
||||
fn update_weights(&self, rewards_json: &str) -> PyResult<()> {
|
||||
let rewards: Vec<(String, f64)> = serde_json::from_str(rewards_json)
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
|
||||
self.inner.update_weights(&rewards);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,218 +1,33 @@
|
||||
//! PyO3 bridge — exposes Rust backend to Python.
|
||||
//! PyO3 bridge — exposes ~50 Rust classes to Python via `openjarvis_rust`.
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::PyDict;
|
||||
use std::collections::HashMap;
|
||||
|
||||
// Re-export core types as Python classes
|
||||
// Shared tokio runtime for async-to-sync bridge (agents, future async APIs).
|
||||
pub(crate) static RUNTIME: Lazy<tokio::runtime::Runtime> = Lazy::new(|| {
|
||||
tokio::runtime::Runtime::new().expect("Failed to create tokio runtime")
|
||||
});
|
||||
|
||||
#[pyclass(name = "Message")]
|
||||
#[derive(Clone)]
|
||||
struct PyMessage {
|
||||
#[pyo3(get, set)]
|
||||
role: String,
|
||||
#[pyo3(get, set)]
|
||||
content: String,
|
||||
#[pyo3(get, set)]
|
||||
name: Option<String>,
|
||||
#[pyo3(get, set)]
|
||||
tool_call_id: Option<String>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyMessage {
|
||||
#[new]
|
||||
fn new(role: String, content: String) -> Self {
|
||||
Self {
|
||||
role,
|
||||
content,
|
||||
name: None,
|
||||
tool_call_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PyMessage {
|
||||
fn to_core(&self) -> openjarvis_core::Message {
|
||||
let role = match self.role.as_str() {
|
||||
"system" => openjarvis_core::Role::System,
|
||||
"assistant" => openjarvis_core::Role::Assistant,
|
||||
"tool" => openjarvis_core::Role::Tool,
|
||||
_ => openjarvis_core::Role::User,
|
||||
};
|
||||
openjarvis_core::Message {
|
||||
role,
|
||||
content: self.content.clone(),
|
||||
name: self.name.clone(),
|
||||
tool_calls: None,
|
||||
tool_call_id: self.tool_call_id.clone(),
|
||||
metadata: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "ToolResult")]
|
||||
#[derive(Clone)]
|
||||
struct PyToolResult {
|
||||
#[pyo3(get)]
|
||||
tool_name: String,
|
||||
#[pyo3(get)]
|
||||
content: String,
|
||||
#[pyo3(get)]
|
||||
success: bool,
|
||||
}
|
||||
|
||||
#[pyclass(name = "Config")]
|
||||
struct PyConfig {
|
||||
inner: openjarvis_core::JarvisConfig,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyConfig {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: openjarvis_core::JarvisConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!(
|
||||
"Config(engine={}, model={})",
|
||||
self.inner.engine.default, self.inner.intelligence.default_model
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "OllamaEngine")]
|
||||
struct PyOllamaEngine {
|
||||
inner: openjarvis_engine::OllamaEngine,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyOllamaEngine {
|
||||
#[new]
|
||||
#[pyo3(signature = (host="http://localhost:11434", timeout=120.0))]
|
||||
fn new(host: &str, timeout: f64) -> Self {
|
||||
Self {
|
||||
inner: openjarvis_engine::OllamaEngine::new(host, timeout),
|
||||
}
|
||||
}
|
||||
|
||||
fn engine_id(&self) -> &str {
|
||||
use openjarvis_engine::InferenceEngine;
|
||||
self.inner.engine_id()
|
||||
}
|
||||
|
||||
fn health(&self) -> bool {
|
||||
use openjarvis_engine::InferenceEngine;
|
||||
self.inner.health()
|
||||
}
|
||||
|
||||
fn list_models(&self) -> PyResult<Vec<String>> {
|
||||
use openjarvis_engine::InferenceEngine;
|
||||
self.inner
|
||||
.list_models()
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
|
||||
}
|
||||
|
||||
#[pyo3(signature = (messages, model, temperature=0.7, max_tokens=1024))]
|
||||
fn generate(
|
||||
&self,
|
||||
messages: Vec<PyMessage>,
|
||||
model: &str,
|
||||
temperature: f64,
|
||||
max_tokens: i64,
|
||||
) -> PyResult<String> {
|
||||
use openjarvis_engine::InferenceEngine;
|
||||
let core_msgs: Vec<openjarvis_core::Message> =
|
||||
messages.iter().map(|m| m.to_core()).collect();
|
||||
let result = self
|
||||
.inner
|
||||
.generate(&core_msgs, model, temperature, max_tokens, None)
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(serde_json::to_string(&result).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "SecretScanner")]
|
||||
struct PySecretScanner {
|
||||
inner: openjarvis_security::SecretScanner,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PySecretScanner {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: openjarvis_security::SecretScanner::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn scan(&self, text: &str) -> PyResult<String> {
|
||||
let result = self.inner.scan(text);
|
||||
Ok(serde_json::to_string(&result).unwrap_or_default())
|
||||
}
|
||||
|
||||
fn redact(&self, text: &str) -> String {
|
||||
self.inner.redact(text)
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "PIIScanner")]
|
||||
struct PyPIIScanner {
|
||||
inner: openjarvis_security::PIIScanner,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyPIIScanner {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: openjarvis_security::PIIScanner::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn scan(&self, text: &str) -> PyResult<String> {
|
||||
let result = self.inner.scan(text);
|
||||
Ok(serde_json::to_string(&result).unwrap_or_default())
|
||||
}
|
||||
|
||||
fn redact(&self, text: &str) -> String {
|
||||
self.inner.redact(text)
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "CalculatorTool")]
|
||||
struct PyCalculatorTool;
|
||||
|
||||
#[pymethods]
|
||||
impl PyCalculatorTool {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn execute(&self, expression: &str) -> PyResult<String> {
|
||||
use openjarvis_tools::traits::BaseTool;
|
||||
let tool = openjarvis_tools::builtin::calculator::CalculatorTool;
|
||||
let params = serde_json::json!({"expression": expression});
|
||||
let result = tool
|
||||
.execute(¶ms)
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(result.content)
|
||||
}
|
||||
}
|
||||
pub mod agents;
|
||||
pub mod core;
|
||||
pub mod engine;
|
||||
pub mod learning;
|
||||
pub mod mcp;
|
||||
pub mod security;
|
||||
pub mod storage;
|
||||
pub mod telemetry;
|
||||
pub mod tools;
|
||||
pub mod traces;
|
||||
|
||||
// Module-level functions
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (path=None))]
|
||||
fn load_config(path: Option<&str>) -> PyResult<PyConfig> {
|
||||
fn load_config(path: Option<&str>) -> PyResult<core::PyConfig> {
|
||||
let p = path.map(std::path::Path::new);
|
||||
let config = openjarvis_core::load_config(p)
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(PyConfig { inner: config })
|
||||
Ok(core::PyConfig { inner: config })
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
@@ -233,16 +48,77 @@ fn is_sensitive_file(path: &str) -> bool {
|
||||
|
||||
#[pymodule]
|
||||
fn openjarvis_rust(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyMessage>()?;
|
||||
m.add_class::<PyToolResult>()?;
|
||||
m.add_class::<PyConfig>()?;
|
||||
m.add_class::<PyOllamaEngine>()?;
|
||||
m.add_class::<PySecretScanner>()?;
|
||||
m.add_class::<PyPIIScanner>()?;
|
||||
m.add_class::<PyCalculatorTool>()?;
|
||||
// --- Core types ---
|
||||
m.add_class::<core::PyMessage>()?;
|
||||
m.add_class::<core::PyToolResult>()?;
|
||||
m.add_class::<core::PyToolCall>()?;
|
||||
m.add_class::<core::PyConfig>()?;
|
||||
m.add_class::<core::PyEventBus>()?;
|
||||
m.add_class::<core::PyModelSpec>()?;
|
||||
m.add_class::<core::PyRoutingContext>()?;
|
||||
m.add_class::<core::PyAgentContext>()?;
|
||||
m.add_class::<core::PyAgentResult>()?;
|
||||
|
||||
// --- Engines ---
|
||||
m.add_class::<engine::PyEngine>()?;
|
||||
m.add_class::<engine::PyOllamaEngine>()?;
|
||||
|
||||
// --- Agents ---
|
||||
m.add_class::<agents::PySimpleAgent>()?;
|
||||
m.add_class::<agents::PyOrchestratorAgent>()?;
|
||||
m.add_class::<agents::PyNativeReActAgent>()?;
|
||||
m.add_class::<agents::PyLoopGuard>()?;
|
||||
|
||||
// --- Tools ---
|
||||
m.add_class::<tools::PyToolExecutor>()?;
|
||||
m.add_class::<tools::PyCalculatorTool>()?;
|
||||
m.add_class::<tools::PyThinkTool>()?;
|
||||
m.add_class::<tools::PyFileReadTool>()?;
|
||||
m.add_class::<tools::PyFileWriteTool>()?;
|
||||
m.add_class::<tools::PyShellExecTool>()?;
|
||||
m.add_class::<tools::PyHttpRequestTool>()?;
|
||||
m.add_class::<tools::PyGitStatusTool>()?;
|
||||
m.add_class::<tools::PyGitDiffTool>()?;
|
||||
m.add_class::<tools::PyGitLogTool>()?;
|
||||
|
||||
// --- Storage / Memory ---
|
||||
m.add_class::<storage::PySQLiteMemory>()?;
|
||||
m.add_class::<storage::PyBM25Memory>()?;
|
||||
m.add_class::<storage::PyKnowledgeGraphMemory>()?;
|
||||
|
||||
// --- Security ---
|
||||
m.add_class::<security::PySecretScanner>()?;
|
||||
m.add_class::<security::PyPIIScanner>()?;
|
||||
m.add_class::<security::PyGuardrailsEngine>()?;
|
||||
m.add_class::<security::PyAuditLogger>()?;
|
||||
m.add_class::<security::PyCapabilityPolicy>()?;
|
||||
m.add_class::<security::PyInjectionScanner>()?;
|
||||
m.add_class::<security::PyRateLimiter>()?;
|
||||
m.add_class::<security::PyTaintSet>()?;
|
||||
|
||||
// --- Telemetry ---
|
||||
m.add_class::<telemetry::PyTelemetryStore>()?;
|
||||
m.add_class::<telemetry::PyTelemetryAggregator>()?;
|
||||
m.add_class::<telemetry::PyInstrumentedEngine>()?;
|
||||
|
||||
// --- Traces ---
|
||||
m.add_class::<traces::PyTraceStore>()?;
|
||||
m.add_class::<traces::PyTraceCollector>()?;
|
||||
m.add_class::<traces::PyTraceAnalyzer>()?;
|
||||
|
||||
// --- Learning ---
|
||||
m.add_class::<learning::PyHeuristicRouter>()?;
|
||||
m.add_class::<learning::PyBanditRouterPolicy>()?;
|
||||
m.add_class::<learning::PyGRPORouterPolicy>()?;
|
||||
|
||||
// --- MCP ---
|
||||
m.add_class::<mcp::PyMcpServer>()?;
|
||||
|
||||
// --- Module-level functions ---
|
||||
m.add_function(wrap_pyfunction!(load_config, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(detect_hardware, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(check_ssrf, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(is_sensitive_file, m)?)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
//! PyO3 bindings for the MCP server.
|
||||
|
||||
use crate::tools::PyToolExecutor;
|
||||
use pyo3::prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[pyclass(name = "McpServer")]
|
||||
pub struct PyMcpServer {
|
||||
inner: openjarvis_mcp::McpServer,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyMcpServer {
|
||||
#[new]
|
||||
fn new(executor: &PyToolExecutor) -> Self {
|
||||
Self {
|
||||
inner: openjarvis_mcp::McpServer::new(Arc::clone(&executor.inner)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Process a JSON-RPC request string and return a JSON-RPC response string.
|
||||
fn handle_json(&self, json_str: &str) -> String {
|
||||
self.inner.handle_json(json_str)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
//! PyO3 bindings for security types.
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[pyclass(name = "SecretScanner")]
|
||||
pub struct PySecretScanner {
|
||||
inner: openjarvis_security::SecretScanner,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PySecretScanner {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: openjarvis_security::SecretScanner::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn scan(&self, text: &str) -> PyResult<String> {
|
||||
let result = self.inner.scan(text);
|
||||
Ok(serde_json::to_string(&result).unwrap_or_default())
|
||||
}
|
||||
|
||||
fn redact(&self, text: &str) -> String {
|
||||
self.inner.redact(text)
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "PIIScanner")]
|
||||
pub struct PyPIIScanner {
|
||||
inner: openjarvis_security::PIIScanner,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyPIIScanner {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: openjarvis_security::PIIScanner::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn scan(&self, text: &str) -> PyResult<String> {
|
||||
let result = self.inner.scan(text);
|
||||
Ok(serde_json::to_string(&result).unwrap_or_default())
|
||||
}
|
||||
|
||||
fn redact(&self, text: &str) -> String {
|
||||
self.inner.redact(text)
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "GuardrailsEngine")]
|
||||
pub struct PyGuardrailsEngine {
|
||||
inner: openjarvis_security::GuardrailsEngine<openjarvis_engine::Engine>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyGuardrailsEngine {
|
||||
#[new]
|
||||
#[pyo3(signature = (engine_key="ollama", host="http://localhost:11434", mode="warn", scan_input=true, scan_output=true))]
|
||||
fn new(
|
||||
engine_key: &str,
|
||||
host: &str,
|
||||
mode: &str,
|
||||
scan_input: bool,
|
||||
scan_output: bool,
|
||||
) -> PyResult<Self> {
|
||||
let config = openjarvis_core::JarvisConfig::default();
|
||||
let engine = openjarvis_engine::get_engine_static(&config, Some(engine_key))
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
let redaction_mode = match mode {
|
||||
"redact" => openjarvis_security::RedactionMode::Redact,
|
||||
"block" => openjarvis_security::RedactionMode::Block,
|
||||
_ => openjarvis_security::RedactionMode::Warn,
|
||||
};
|
||||
Ok(Self {
|
||||
inner: openjarvis_security::GuardrailsEngine::new(
|
||||
engine,
|
||||
redaction_mode,
|
||||
scan_input,
|
||||
scan_output,
|
||||
None,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn engine_id(&self) -> &str {
|
||||
use openjarvis_engine::InferenceEngine;
|
||||
self.inner.engine_id()
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "AuditLogger")]
|
||||
pub struct PyAuditLogger {
|
||||
inner: parking_lot::Mutex<openjarvis_security::AuditLogger>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyAuditLogger {
|
||||
#[new]
|
||||
#[pyo3(signature = (path=None))]
|
||||
fn new(path: Option<&str>) -> PyResult<Self> {
|
||||
let db_path = match path {
|
||||
Some(p) => std::path::PathBuf::from(p),
|
||||
None => std::path::PathBuf::from(":memory:"),
|
||||
};
|
||||
let inner = openjarvis_security::AuditLogger::new(&db_path)
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(Self {
|
||||
inner: parking_lot::Mutex::new(inner),
|
||||
})
|
||||
}
|
||||
|
||||
fn count(&self) -> i64 {
|
||||
self.inner.lock().count()
|
||||
}
|
||||
|
||||
fn verify_chain(&self) -> PyResult<(bool, Option<i64>)> {
|
||||
self.inner
|
||||
.lock()
|
||||
.verify_chain()
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
|
||||
}
|
||||
|
||||
fn tail_hash(&self) -> String {
|
||||
self.inner.lock().tail_hash()
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "CapabilityPolicy")]
|
||||
pub struct PyCapabilityPolicy {
|
||||
inner: openjarvis_security::CapabilityPolicy,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyCapabilityPolicy {
|
||||
#[new]
|
||||
#[pyo3(signature = (default_deny=true))]
|
||||
fn new(default_deny: bool) -> Self {
|
||||
Self {
|
||||
inner: openjarvis_security::CapabilityPolicy::new(default_deny),
|
||||
}
|
||||
}
|
||||
|
||||
fn check(&self, agent_id: &str, capability: &str, resource: &str) -> bool {
|
||||
self.inner.check(agent_id, capability, resource)
|
||||
}
|
||||
|
||||
fn grant(&mut self, agent_id: &str, capability: &str, pattern: &str) {
|
||||
self.inner.grant(agent_id, capability, pattern);
|
||||
}
|
||||
|
||||
fn deny(&mut self, agent_id: &str, capability: &str) {
|
||||
self.inner.deny(agent_id, capability);
|
||||
}
|
||||
|
||||
fn list_agents(&self) -> Vec<String> {
|
||||
self.inner.list_agents()
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "InjectionScanner")]
|
||||
pub struct PyInjectionScanner {
|
||||
inner: openjarvis_security::InjectionScanner,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyInjectionScanner {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: openjarvis_security::InjectionScanner::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn scan(&self, text: &str) -> PyResult<String> {
|
||||
let result = self.inner.scan(text);
|
||||
// InjectionScanResult doesn't derive Serialize, so format manually.
|
||||
Ok(serde_json::json!({
|
||||
"is_clean": result.is_clean,
|
||||
"threat_level": format!("{:?}", result.threat_level),
|
||||
"findings_count": result.findings.len(),
|
||||
})
|
||||
.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "RateLimiter")]
|
||||
pub struct PyRateLimiter {
|
||||
inner: openjarvis_security::RateLimiter,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyRateLimiter {
|
||||
#[new]
|
||||
#[pyo3(signature = (requests_per_minute=60, burst_size=10))]
|
||||
fn new(requests_per_minute: u32, burst_size: u32) -> Self {
|
||||
Self {
|
||||
inner: openjarvis_security::RateLimiter::new(
|
||||
openjarvis_security::RateLimitConfig {
|
||||
requests_per_minute,
|
||||
burst_size,
|
||||
enabled: true,
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns (allowed, wait_seconds).
|
||||
fn check(&self, key: &str) -> (bool, f64) {
|
||||
self.inner.check(key)
|
||||
}
|
||||
|
||||
fn reset(&self, key: Option<&str>) {
|
||||
self.inner.reset(key);
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "TaintSet")]
|
||||
pub struct PyTaintSet {
|
||||
inner: openjarvis_security::TaintSet,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyTaintSet {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: openjarvis_security::TaintSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn add(&mut self, label: &str) {
|
||||
let taint_label = match label {
|
||||
"pii" => openjarvis_security::TaintLabel::Pii,
|
||||
"secret" => openjarvis_security::TaintLabel::Secret,
|
||||
"user_private" => openjarvis_security::TaintLabel::UserPrivate,
|
||||
"external" => openjarvis_security::TaintLabel::External,
|
||||
_ => openjarvis_security::TaintLabel::External,
|
||||
};
|
||||
// TaintSet is immutable-style; union with a single-label set.
|
||||
self.inner = self.inner.union(
|
||||
&openjarvis_security::TaintSet::from_labels(&[taint_label]),
|
||||
);
|
||||
}
|
||||
|
||||
fn has(&self, label: &str) -> bool {
|
||||
let taint_label = match label {
|
||||
"pii" => openjarvis_security::TaintLabel::Pii,
|
||||
"secret" => openjarvis_security::TaintLabel::Secret,
|
||||
"user_private" => openjarvis_security::TaintLabel::UserPrivate,
|
||||
"external" => openjarvis_security::TaintLabel::External,
|
||||
_ => return false,
|
||||
};
|
||||
self.inner.has(taint_label)
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.inner.is_empty()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
//! PyO3 bindings for storage/memory backends.
|
||||
|
||||
use openjarvis_tools::storage::MemoryBackend;
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[pyclass(name = "SQLiteMemory")]
|
||||
pub struct PySQLiteMemory {
|
||||
inner: openjarvis_tools::storage::SQLiteMemory,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PySQLiteMemory {
|
||||
#[new]
|
||||
#[pyo3(signature = (path=":memory:"))]
|
||||
fn new(path: &str) -> PyResult<Self> {
|
||||
let inner = openjarvis_tools::storage::SQLiteMemory::new(std::path::Path::new(path))
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
fn backend_id(&self) -> &str {
|
||||
self.inner.backend_id()
|
||||
}
|
||||
|
||||
#[pyo3(signature = (content, source, metadata=None))]
|
||||
fn store(&self, content: &str, source: &str, metadata: Option<&str>) -> PyResult<String> {
|
||||
let meta = metadata
|
||||
.map(|m| serde_json::from_str(m))
|
||||
.transpose()
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
|
||||
self.inner
|
||||
.store(content, source, meta.as_ref())
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
|
||||
}
|
||||
|
||||
#[pyo3(signature = (query, top_k=5))]
|
||||
fn retrieve(&self, query: &str, top_k: usize) -> PyResult<String> {
|
||||
let results = self
|
||||
.inner
|
||||
.retrieve(query, top_k)
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(serde_json::to_string(&results).unwrap_or_default())
|
||||
}
|
||||
|
||||
fn count(&self) -> PyResult<usize> {
|
||||
self.inner
|
||||
.count()
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
|
||||
}
|
||||
|
||||
fn clear(&self) -> PyResult<()> {
|
||||
self.inner
|
||||
.clear()
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "BM25Memory")]
|
||||
pub struct PyBM25Memory {
|
||||
inner: openjarvis_tools::storage::BM25Memory,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyBM25Memory {
|
||||
#[new]
|
||||
#[pyo3(signature = (k1=1.2, b=0.75))]
|
||||
fn new(k1: f64, b: f64) -> Self {
|
||||
Self {
|
||||
inner: openjarvis_tools::storage::BM25Memory::new(k1, b),
|
||||
}
|
||||
}
|
||||
|
||||
fn backend_id(&self) -> &str {
|
||||
self.inner.backend_id()
|
||||
}
|
||||
|
||||
#[pyo3(signature = (content, source, metadata=None))]
|
||||
fn store(&self, content: &str, source: &str, metadata: Option<&str>) -> PyResult<String> {
|
||||
let meta = metadata
|
||||
.map(|m| serde_json::from_str(m))
|
||||
.transpose()
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
|
||||
self.inner
|
||||
.store(content, source, meta.as_ref())
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
|
||||
}
|
||||
|
||||
#[pyo3(signature = (query, top_k=5))]
|
||||
fn retrieve(&self, query: &str, top_k: usize) -> PyResult<String> {
|
||||
let results = self
|
||||
.inner
|
||||
.retrieve(query, top_k)
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(serde_json::to_string(&results).unwrap_or_default())
|
||||
}
|
||||
|
||||
fn count(&self) -> PyResult<usize> {
|
||||
self.inner
|
||||
.count()
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "KnowledgeGraphMemory")]
|
||||
pub struct PyKnowledgeGraphMemory {
|
||||
inner: openjarvis_tools::storage::KnowledgeGraphMemory,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyKnowledgeGraphMemory {
|
||||
#[new]
|
||||
#[pyo3(signature = (path=":memory:"))]
|
||||
fn new(path: &str) -> PyResult<Self> {
|
||||
let inner = openjarvis_tools::storage::KnowledgeGraphMemory::new(std::path::Path::new(path))
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
fn backend_id(&self) -> &str {
|
||||
self.inner.backend_id()
|
||||
}
|
||||
|
||||
#[pyo3(signature = (content, source, metadata=None))]
|
||||
fn store(&self, content: &str, source: &str, metadata: Option<&str>) -> PyResult<String> {
|
||||
let meta = metadata
|
||||
.map(|m| serde_json::from_str(m))
|
||||
.transpose()
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
|
||||
self.inner
|
||||
.store(content, source, meta.as_ref())
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
|
||||
}
|
||||
|
||||
#[pyo3(signature = (query, top_k=5))]
|
||||
fn retrieve(&self, query: &str, top_k: usize) -> PyResult<String> {
|
||||
let results = self
|
||||
.inner
|
||||
.retrieve(query, top_k)
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(serde_json::to_string(&results).unwrap_or_default())
|
||||
}
|
||||
|
||||
fn count(&self) -> PyResult<usize> {
|
||||
self.inner
|
||||
.count()
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
//! PyO3 bindings for telemetry types.
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[pyclass(name = "TelemetryStore")]
|
||||
pub struct PyTelemetryStore {
|
||||
pub inner: Arc<openjarvis_telemetry::TelemetryStore>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyTelemetryStore {
|
||||
#[new]
|
||||
#[pyo3(signature = (path=None))]
|
||||
fn new(path: Option<&str>) -> PyResult<Self> {
|
||||
let inner = match path {
|
||||
Some(p) => openjarvis_telemetry::TelemetryStore::new(std::path::Path::new(p)),
|
||||
None => openjarvis_telemetry::TelemetryStore::in_memory(),
|
||||
}
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(Self {
|
||||
inner: Arc::new(inner),
|
||||
})
|
||||
}
|
||||
|
||||
fn count(&self) -> PyResult<usize> {
|
||||
self.inner
|
||||
.count()
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
|
||||
}
|
||||
|
||||
fn clear(&self) -> PyResult<()> {
|
||||
self.inner
|
||||
.clear()
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// TelemetryAggregator computes aggregate stats from a TelemetryStore.
|
||||
/// The Rust type is a unit struct with a static method.
|
||||
#[pyclass(name = "TelemetryAggregator")]
|
||||
pub struct PyTelemetryAggregator {
|
||||
store: Arc<openjarvis_telemetry::TelemetryStore>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyTelemetryAggregator {
|
||||
#[new]
|
||||
fn new(store: &PyTelemetryStore) -> Self {
|
||||
Self {
|
||||
store: Arc::clone(&store.inner),
|
||||
}
|
||||
}
|
||||
|
||||
fn stats(&self) -> PyResult<String> {
|
||||
let stats = openjarvis_telemetry::TelemetryAggregator::stats(&self.store)
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(serde_json::to_string(&stats).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "InstrumentedEngine")]
|
||||
pub struct PyInstrumentedEngine {
|
||||
inner: openjarvis_telemetry::InstrumentedEngine<openjarvis_engine::Engine>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyInstrumentedEngine {
|
||||
#[new]
|
||||
#[pyo3(signature = (engine_key="ollama", host="http://localhost:11434", store_path=None, agent_name="default"))]
|
||||
fn new(engine_key: &str, host: &str, store_path: Option<&str>, agent_name: &str) -> PyResult<Self> {
|
||||
let config = openjarvis_core::JarvisConfig::default();
|
||||
let engine = openjarvis_engine::get_engine_static(&config, Some(engine_key))
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
let store = Arc::new(match store_path {
|
||||
Some(p) => openjarvis_telemetry::TelemetryStore::new(std::path::Path::new(p)),
|
||||
None => openjarvis_telemetry::TelemetryStore::in_memory(),
|
||||
}
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?);
|
||||
Ok(Self {
|
||||
inner: openjarvis_telemetry::InstrumentedEngine::new(
|
||||
engine,
|
||||
store,
|
||||
agent_name.to_string(),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn engine_id(&self) -> &str {
|
||||
use openjarvis_engine::InferenceEngine;
|
||||
self.inner.engine_id()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
//! PyO3 bindings for tool types.
|
||||
|
||||
use openjarvis_tools::traits::BaseTool;
|
||||
use pyo3::prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[pyclass(name = "ToolExecutor")]
|
||||
pub struct PyToolExecutor {
|
||||
pub inner: Arc<openjarvis_tools::ToolExecutor>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyToolExecutor {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(openjarvis_tools::ToolExecutor::new(None, None)),
|
||||
}
|
||||
}
|
||||
|
||||
fn list_tools(&self) -> Vec<String> {
|
||||
self.inner.list_tools()
|
||||
}
|
||||
|
||||
fn execute(&self, tool_name: &str, params_json: &str) -> PyResult<String> {
|
||||
let params: serde_json::Value = serde_json::from_str(params_json)
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
|
||||
let result = self
|
||||
.inner
|
||||
.execute(tool_name, ¶ms, None, None)
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(serde_json::to_string(&result).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "CalculatorTool")]
|
||||
pub struct PyCalculatorTool;
|
||||
|
||||
#[pymethods]
|
||||
impl PyCalculatorTool {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn execute(&self, expression: &str) -> PyResult<String> {
|
||||
let tool = openjarvis_tools::builtin::calculator::CalculatorTool;
|
||||
let params = serde_json::json!({"expression": expression});
|
||||
let result = tool
|
||||
.execute(¶ms)
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(result.content)
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "ThinkTool")]
|
||||
pub struct PyThinkTool;
|
||||
|
||||
#[pymethods]
|
||||
impl PyThinkTool {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn execute(&self, thought: &str) -> PyResult<String> {
|
||||
let tool = openjarvis_tools::builtin::think::ThinkTool;
|
||||
let params = serde_json::json!({"thought": thought});
|
||||
let result = tool
|
||||
.execute(¶ms)
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(result.content)
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "FileReadTool")]
|
||||
pub struct PyFileReadTool;
|
||||
|
||||
#[pymethods]
|
||||
impl PyFileReadTool {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn execute(&self, path: &str) -> PyResult<String> {
|
||||
let tool = openjarvis_tools::builtin::file_tools::FileReadTool;
|
||||
let params = serde_json::json!({"path": path});
|
||||
let result = tool
|
||||
.execute(¶ms)
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(result.content)
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "FileWriteTool")]
|
||||
pub struct PyFileWriteTool;
|
||||
|
||||
#[pymethods]
|
||||
impl PyFileWriteTool {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn execute(&self, path: &str, content: &str) -> PyResult<String> {
|
||||
let tool = openjarvis_tools::builtin::file_tools::FileWriteTool;
|
||||
let params = serde_json::json!({"path": path, "content": content});
|
||||
let result = tool
|
||||
.execute(¶ms)
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(result.content)
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "ShellExecTool")]
|
||||
pub struct PyShellExecTool;
|
||||
|
||||
#[pymethods]
|
||||
impl PyShellExecTool {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
#[pyo3(signature = (command, cwd=None))]
|
||||
fn execute(&self, command: &str, cwd: Option<&str>) -> PyResult<String> {
|
||||
let tool = openjarvis_tools::builtin::shell::ShellExecTool;
|
||||
let mut params = serde_json::json!({"command": command});
|
||||
if let Some(cwd) = cwd {
|
||||
params["cwd"] = serde_json::Value::String(cwd.to_string());
|
||||
}
|
||||
let result = tool
|
||||
.execute(¶ms)
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(result.content)
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "HttpRequestTool")]
|
||||
pub struct PyHttpRequestTool;
|
||||
|
||||
#[pymethods]
|
||||
impl PyHttpRequestTool {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
#[pyo3(signature = (url, method="GET", body=None))]
|
||||
fn execute(&self, url: &str, method: &str, body: Option<&str>) -> PyResult<String> {
|
||||
let tool = openjarvis_tools::builtin::http_tools::HttpRequestTool;
|
||||
let mut params = serde_json::json!({"url": url, "method": method});
|
||||
if let Some(body) = body {
|
||||
params["body"] = serde_json::Value::String(body.to_string());
|
||||
}
|
||||
let result = tool
|
||||
.execute(¶ms)
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(result.content)
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "GitStatusTool")]
|
||||
pub struct PyGitStatusTool;
|
||||
|
||||
#[pymethods]
|
||||
impl PyGitStatusTool {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
#[pyo3(signature = (cwd=None))]
|
||||
fn execute(&self, cwd: Option<&str>) -> PyResult<String> {
|
||||
let tool = openjarvis_tools::builtin::git_tools::GitStatusTool;
|
||||
let mut params = serde_json::json!({});
|
||||
if let Some(cwd) = cwd {
|
||||
params["cwd"] = serde_json::Value::String(cwd.to_string());
|
||||
}
|
||||
let result = tool
|
||||
.execute(¶ms)
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(result.content)
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "GitDiffTool")]
|
||||
pub struct PyGitDiffTool;
|
||||
|
||||
#[pymethods]
|
||||
impl PyGitDiffTool {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
#[pyo3(signature = (cwd=None))]
|
||||
fn execute(&self, cwd: Option<&str>) -> PyResult<String> {
|
||||
let tool = openjarvis_tools::builtin::git_tools::GitDiffTool;
|
||||
let mut params = serde_json::json!({});
|
||||
if let Some(cwd) = cwd {
|
||||
params["cwd"] = serde_json::Value::String(cwd.to_string());
|
||||
}
|
||||
let result = tool
|
||||
.execute(¶ms)
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(result.content)
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "GitLogTool")]
|
||||
pub struct PyGitLogTool;
|
||||
|
||||
#[pymethods]
|
||||
impl PyGitLogTool {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
#[pyo3(signature = (cwd=None, count=None))]
|
||||
fn execute(&self, cwd: Option<&str>, count: Option<u32>) -> PyResult<String> {
|
||||
let tool = openjarvis_tools::builtin::git_tools::GitLogTool;
|
||||
let mut params = serde_json::json!({});
|
||||
if let Some(cwd) = cwd {
|
||||
params["cwd"] = serde_json::Value::String(cwd.to_string());
|
||||
}
|
||||
if let Some(count) = count {
|
||||
params["count"] = serde_json::Value::Number(count.into());
|
||||
}
|
||||
let result = tool
|
||||
.execute(¶ms)
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(result.content)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
//! PyO3 bindings for trace types.
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[pyclass(name = "TraceStore")]
|
||||
pub struct PyTraceStore {
|
||||
pub inner: Arc<openjarvis_traces::TraceStore>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyTraceStore {
|
||||
#[new]
|
||||
#[pyo3(signature = (path=None))]
|
||||
fn new(path: Option<&str>) -> PyResult<Self> {
|
||||
let inner = match path {
|
||||
Some(p) => openjarvis_traces::TraceStore::new(std::path::Path::new(p)),
|
||||
None => openjarvis_traces::TraceStore::in_memory(),
|
||||
}
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(Self {
|
||||
inner: Arc::new(inner),
|
||||
})
|
||||
}
|
||||
|
||||
fn count(&self) -> PyResult<usize> {
|
||||
self.inner
|
||||
.count()
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "TraceCollector")]
|
||||
pub struct PyTraceCollector {
|
||||
inner: openjarvis_traces::TraceCollector,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyTraceCollector {
|
||||
#[new]
|
||||
fn new(store: &PyTraceStore) -> Self {
|
||||
Self {
|
||||
inner: openjarvis_traces::TraceCollector::new(Arc::clone(&store.inner)),
|
||||
}
|
||||
}
|
||||
|
||||
fn active_count(&self) -> usize {
|
||||
self.inner.active_count()
|
||||
}
|
||||
}
|
||||
|
||||
/// TraceAnalyzer wraps stats computation over a TraceStore.
|
||||
/// Since the Rust TraceAnalyzer has a lifetime parameter, we own the store
|
||||
/// and create the analyzer on each call.
|
||||
#[pyclass(name = "TraceAnalyzer")]
|
||||
pub struct PyTraceAnalyzer {
|
||||
store: Arc<openjarvis_traces::TraceStore>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyTraceAnalyzer {
|
||||
#[new]
|
||||
fn new(store: &PyTraceStore) -> Self {
|
||||
Self {
|
||||
store: Arc::clone(&store.inner),
|
||||
}
|
||||
}
|
||||
|
||||
fn stats(&self) -> PyResult<String> {
|
||||
let analyzer = openjarvis_traces::TraceAnalyzer::new(&self.store);
|
||||
let stats = analyzer
|
||||
.overall_stats()
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(serde_json::to_string(&stats).unwrap_or_default())
|
||||
}
|
||||
|
||||
fn stats_by_agent(&self) -> PyResult<String> {
|
||||
let analyzer = openjarvis_traces::TraceAnalyzer::new(&self.store);
|
||||
let stats = analyzer
|
||||
.stats_by_agent()
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(serde_json::to_string(&stats).unwrap_or_default())
|
||||
}
|
||||
|
||||
fn stats_by_model(&self) -> PyResult<String> {
|
||||
let analyzer = openjarvis_traces::TraceAnalyzer::new(&self.store);
|
||||
let stats = analyzer
|
||||
.stats_by_model()
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
|
||||
Ok(serde_json::to_string(&stats).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,10 @@ use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Wraps an existing `InferenceEngine` with security scanning on I/O.
|
||||
pub struct GuardrailsEngine {
|
||||
engine: Arc<dyn InferenceEngine>,
|
||||
///
|
||||
/// Generic over `E` for static dispatch when the engine type is known.
|
||||
pub struct GuardrailsEngine<E: InferenceEngine> {
|
||||
engine: E,
|
||||
secret_scanner: SecretScanner,
|
||||
pii_scanner: PIIScanner,
|
||||
mode: RedactionMode,
|
||||
@@ -19,9 +21,9 @@ pub struct GuardrailsEngine {
|
||||
bus: Option<Arc<EventBus>>,
|
||||
}
|
||||
|
||||
impl GuardrailsEngine {
|
||||
impl<E: InferenceEngine> GuardrailsEngine<E> {
|
||||
pub fn new(
|
||||
engine: Arc<dyn InferenceEngine>,
|
||||
engine: E,
|
||||
mode: RedactionMode,
|
||||
scan_input: bool,
|
||||
scan_output: bool,
|
||||
@@ -125,7 +127,7 @@ impl GuardrailsEngine {
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl InferenceEngine for GuardrailsEngine {
|
||||
impl<E: InferenceEngine> InferenceEngine for GuardrailsEngine<E> {
|
||||
fn engine_id(&self) -> &str {
|
||||
self.engine.engine_id()
|
||||
}
|
||||
@@ -231,7 +233,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_guardrails_warn_mode() {
|
||||
let engine = Arc::new(MockEngine);
|
||||
let engine = MockEngine;
|
||||
let guardrails =
|
||||
GuardrailsEngine::new(engine, RedactionMode::Warn, false, true, None);
|
||||
let result = guardrails
|
||||
@@ -242,7 +244,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_guardrails_redact_mode() {
|
||||
let engine = Arc::new(MockEngine);
|
||||
let engine = MockEngine;
|
||||
let guardrails =
|
||||
GuardrailsEngine::new(engine, RedactionMode::Redact, false, true, None);
|
||||
let result = guardrails
|
||||
@@ -254,7 +256,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_guardrails_block_mode() {
|
||||
let engine = Arc::new(MockEngine);
|
||||
let engine = MockEngine;
|
||||
let guardrails =
|
||||
GuardrailsEngine::new(engine, RedactionMode::Block, false, true, None);
|
||||
let err = guardrails
|
||||
|
||||
@@ -8,15 +8,18 @@ use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub struct InstrumentedEngine {
|
||||
inner: Arc<dyn InferenceEngine>,
|
||||
/// Wraps any `InferenceEngine` with telemetry recording.
|
||||
///
|
||||
/// Generic over `E` for static dispatch when the engine type is known.
|
||||
pub struct InstrumentedEngine<E: InferenceEngine> {
|
||||
inner: E,
|
||||
store: Arc<TelemetryStore>,
|
||||
agent_name: String,
|
||||
}
|
||||
|
||||
impl InstrumentedEngine {
|
||||
impl<E: InferenceEngine> InstrumentedEngine<E> {
|
||||
pub fn new(
|
||||
inner: Arc<dyn InferenceEngine>,
|
||||
inner: E,
|
||||
store: Arc<TelemetryStore>,
|
||||
agent_name: String,
|
||||
) -> Self {
|
||||
@@ -36,7 +39,7 @@ impl InstrumentedEngine {
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl InferenceEngine for InstrumentedEngine {
|
||||
impl<E: InferenceEngine> InferenceEngine for InstrumentedEngine<E> {
|
||||
fn engine_id(&self) -> &str {
|
||||
self.inner.engine_id()
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ parking_lot = { workspace = true }
|
||||
meval = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
rig-core = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
pub mod builtin;
|
||||
pub mod executor;
|
||||
pub mod rig_tools;
|
||||
pub mod storage;
|
||||
pub mod traits;
|
||||
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
//! Rig-core tool adapters — typed Args structs implementing rig's `Tool` trait.
|
||||
//!
|
||||
//! Each builtin tool gets a rig-core adapter with compile-time JSON schema
|
||||
//! generation via `schemars::JsonSchema`.
|
||||
|
||||
use rig::completion::request::ToolDefinition;
|
||||
use rig::tool::Tool as RigTool;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Calculator
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct CalculatorArgs {
|
||||
/// Mathematical expression to evaluate.
|
||||
pub expression: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Calculator error: {0}")]
|
||||
pub struct CalculatorError(String);
|
||||
|
||||
pub struct RigCalculatorTool;
|
||||
|
||||
impl RigTool for RigCalculatorTool {
|
||||
type Error = CalculatorError;
|
||||
type Args = CalculatorArgs;
|
||||
type Output = String;
|
||||
|
||||
const NAME: &'static str = "calculator";
|
||||
|
||||
async fn definition(&self, _prompt: String) -> ToolDefinition {
|
||||
ToolDefinition {
|
||||
name: "calculator".into(),
|
||||
description: "Evaluate a mathematical expression".into(),
|
||||
parameters: serde_json::to_value(schemars::schema_for!(CalculatorArgs)).unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
|
||||
use crate::builtin::calculator::CalculatorTool;
|
||||
use crate::traits::BaseTool;
|
||||
let params = serde_json::json!({"expression": args.expression});
|
||||
let result = CalculatorTool
|
||||
.execute(¶ms)
|
||||
.map_err(|e| CalculatorError(e.to_string()))?;
|
||||
Ok(result.content)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Think
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct ThinkArgs {
|
||||
/// Internal reasoning thought to record.
|
||||
pub thought: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Think error: {0}")]
|
||||
pub struct ThinkError(String);
|
||||
|
||||
pub struct RigThinkTool;
|
||||
|
||||
impl RigTool for RigThinkTool {
|
||||
type Error = ThinkError;
|
||||
type Args = ThinkArgs;
|
||||
type Output = String;
|
||||
|
||||
const NAME: &'static str = "think";
|
||||
|
||||
async fn definition(&self, _prompt: String) -> ToolDefinition {
|
||||
ToolDefinition {
|
||||
name: "think".into(),
|
||||
description: "Record an internal reasoning step".into(),
|
||||
parameters: serde_json::to_value(schemars::schema_for!(ThinkArgs)).unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
|
||||
use crate::builtin::think::ThinkTool;
|
||||
use crate::traits::BaseTool;
|
||||
let params = serde_json::json!({"thought": args.thought});
|
||||
let result = ThinkTool
|
||||
.execute(¶ms)
|
||||
.map_err(|e| ThinkError(e.to_string()))?;
|
||||
Ok(result.content)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FileRead
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct FileReadArgs {
|
||||
/// Path to the file to read.
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("FileRead error: {0}")]
|
||||
pub struct FileReadError(String);
|
||||
|
||||
pub struct RigFileReadTool;
|
||||
|
||||
impl RigTool for RigFileReadTool {
|
||||
type Error = FileReadError;
|
||||
type Args = FileReadArgs;
|
||||
type Output = String;
|
||||
|
||||
const NAME: &'static str = "file_read";
|
||||
|
||||
async fn definition(&self, _prompt: String) -> ToolDefinition {
|
||||
ToolDefinition {
|
||||
name: "file_read".into(),
|
||||
description: "Read the contents of a file".into(),
|
||||
parameters: serde_json::to_value(schemars::schema_for!(FileReadArgs)).unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
|
||||
use crate::builtin::file_tools::FileReadTool;
|
||||
use crate::traits::BaseTool;
|
||||
let params = serde_json::json!({"path": args.path});
|
||||
let result = FileReadTool
|
||||
.execute(¶ms)
|
||||
.map_err(|e| FileReadError(e.to_string()))?;
|
||||
Ok(result.content)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FileWrite
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct FileWriteArgs {
|
||||
/// Path to the file to write.
|
||||
pub path: String,
|
||||
/// Content to write to the file.
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("FileWrite error: {0}")]
|
||||
pub struct FileWriteError(String);
|
||||
|
||||
pub struct RigFileWriteTool;
|
||||
|
||||
impl RigTool for RigFileWriteTool {
|
||||
type Error = FileWriteError;
|
||||
type Args = FileWriteArgs;
|
||||
type Output = String;
|
||||
|
||||
const NAME: &'static str = "file_write";
|
||||
|
||||
async fn definition(&self, _prompt: String) -> ToolDefinition {
|
||||
ToolDefinition {
|
||||
name: "file_write".into(),
|
||||
description: "Write content to a file".into(),
|
||||
parameters: serde_json::to_value(schemars::schema_for!(FileWriteArgs)).unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
|
||||
use crate::builtin::file_tools::FileWriteTool;
|
||||
use crate::traits::BaseTool;
|
||||
let params = serde_json::json!({"path": args.path, "content": args.content});
|
||||
let result = FileWriteTool
|
||||
.execute(¶ms)
|
||||
.map_err(|e| FileWriteError(e.to_string()))?;
|
||||
Ok(result.content)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ShellExec
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct ShellExecArgs {
|
||||
/// Shell command to execute.
|
||||
pub command: String,
|
||||
/// Optional working directory.
|
||||
pub cwd: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("ShellExec error: {0}")]
|
||||
pub struct ShellExecError(String);
|
||||
|
||||
pub struct RigShellExecTool;
|
||||
|
||||
impl RigTool for RigShellExecTool {
|
||||
type Error = ShellExecError;
|
||||
type Args = ShellExecArgs;
|
||||
type Output = String;
|
||||
|
||||
const NAME: &'static str = "shell_exec";
|
||||
|
||||
async fn definition(&self, _prompt: String) -> ToolDefinition {
|
||||
ToolDefinition {
|
||||
name: "shell_exec".into(),
|
||||
description: "Execute a shell command".into(),
|
||||
parameters: serde_json::to_value(schemars::schema_for!(ShellExecArgs)).unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
|
||||
use crate::builtin::shell::ShellExecTool;
|
||||
use crate::traits::BaseTool;
|
||||
let mut params = serde_json::json!({"command": args.command});
|
||||
if let Some(cwd) = args.cwd {
|
||||
params["cwd"] = serde_json::Value::String(cwd);
|
||||
}
|
||||
let result = ShellExecTool
|
||||
.execute(¶ms)
|
||||
.map_err(|e| ShellExecError(e.to_string()))?;
|
||||
Ok(result.content)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// HttpRequest
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct HttpRequestArgs {
|
||||
/// URL to send the request to.
|
||||
pub url: String,
|
||||
/// HTTP method (GET, POST, PUT, DELETE, PATCH). Defaults to GET.
|
||||
pub method: Option<String>,
|
||||
/// Optional request body (JSON string).
|
||||
pub body: Option<String>,
|
||||
/// Optional request headers as JSON object.
|
||||
pub headers: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("HttpRequest error: {0}")]
|
||||
pub struct HttpRequestError(String);
|
||||
|
||||
pub struct RigHttpRequestTool;
|
||||
|
||||
impl RigTool for RigHttpRequestTool {
|
||||
type Error = HttpRequestError;
|
||||
type Args = HttpRequestArgs;
|
||||
type Output = String;
|
||||
|
||||
const NAME: &'static str = "http_request";
|
||||
|
||||
async fn definition(&self, _prompt: String) -> ToolDefinition {
|
||||
ToolDefinition {
|
||||
name: "http_request".into(),
|
||||
description: "Send an HTTP request".into(),
|
||||
parameters: serde_json::to_value(schemars::schema_for!(HttpRequestArgs)).unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
|
||||
use crate::builtin::http_tools::HttpRequestTool;
|
||||
use crate::traits::BaseTool;
|
||||
let mut params = serde_json::json!({"url": args.url});
|
||||
if let Some(method) = args.method {
|
||||
params["method"] = serde_json::Value::String(method);
|
||||
}
|
||||
if let Some(body) = args.body {
|
||||
params["body"] = serde_json::Value::String(body);
|
||||
}
|
||||
if let Some(headers) = args.headers {
|
||||
params["headers"] = headers;
|
||||
}
|
||||
let result = HttpRequestTool
|
||||
.execute(¶ms)
|
||||
.map_err(|e| HttpRequestError(e.to_string()))?;
|
||||
Ok(result.content)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Git tools
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct GitStatusArgs {
|
||||
/// Optional working directory for git.
|
||||
pub cwd: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("GitStatus error: {0}")]
|
||||
pub struct GitStatusError(String);
|
||||
|
||||
pub struct RigGitStatusTool;
|
||||
|
||||
impl RigTool for RigGitStatusTool {
|
||||
type Error = GitStatusError;
|
||||
type Args = GitStatusArgs;
|
||||
type Output = String;
|
||||
|
||||
const NAME: &'static str = "git_status";
|
||||
|
||||
async fn definition(&self, _prompt: String) -> ToolDefinition {
|
||||
ToolDefinition {
|
||||
name: "git_status".into(),
|
||||
description: "Show git working tree status".into(),
|
||||
parameters: serde_json::to_value(schemars::schema_for!(GitStatusArgs)).unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
|
||||
use crate::builtin::git_tools::GitStatusTool;
|
||||
use crate::traits::BaseTool;
|
||||
let mut params = serde_json::json!({});
|
||||
if let Some(cwd) = args.cwd {
|
||||
params["cwd"] = serde_json::Value::String(cwd);
|
||||
}
|
||||
let result = GitStatusTool
|
||||
.execute(¶ms)
|
||||
.map_err(|e| GitStatusError(e.to_string()))?;
|
||||
Ok(result.content)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct GitDiffArgs {
|
||||
/// Optional working directory for git.
|
||||
pub cwd: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("GitDiff error: {0}")]
|
||||
pub struct GitDiffError(String);
|
||||
|
||||
pub struct RigGitDiffTool;
|
||||
|
||||
impl RigTool for RigGitDiffTool {
|
||||
type Error = GitDiffError;
|
||||
type Args = GitDiffArgs;
|
||||
type Output = String;
|
||||
|
||||
const NAME: &'static str = "git_diff";
|
||||
|
||||
async fn definition(&self, _prompt: String) -> ToolDefinition {
|
||||
ToolDefinition {
|
||||
name: "git_diff".into(),
|
||||
description: "Show git diff of changes".into(),
|
||||
parameters: serde_json::to_value(schemars::schema_for!(GitDiffArgs)).unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
|
||||
use crate::builtin::git_tools::GitDiffTool;
|
||||
use crate::traits::BaseTool;
|
||||
let mut params = serde_json::json!({});
|
||||
if let Some(cwd) = args.cwd {
|
||||
params["cwd"] = serde_json::Value::String(cwd);
|
||||
}
|
||||
let result = GitDiffTool
|
||||
.execute(¶ms)
|
||||
.map_err(|e| GitDiffError(e.to_string()))?;
|
||||
Ok(result.content)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct GitLogArgs {
|
||||
/// Optional working directory for git.
|
||||
pub cwd: Option<String>,
|
||||
/// Number of commits to show.
|
||||
pub count: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("GitLog error: {0}")]
|
||||
pub struct GitLogError(String);
|
||||
|
||||
pub struct RigGitLogTool;
|
||||
|
||||
impl RigTool for RigGitLogTool {
|
||||
type Error = GitLogError;
|
||||
type Args = GitLogArgs;
|
||||
type Output = String;
|
||||
|
||||
const NAME: &'static str = "git_log";
|
||||
|
||||
async fn definition(&self, _prompt: String) -> ToolDefinition {
|
||||
ToolDefinition {
|
||||
name: "git_log".into(),
|
||||
description: "Show git commit log".into(),
|
||||
parameters: serde_json::to_value(schemars::schema_for!(GitLogArgs)).unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
|
||||
use crate::builtin::git_tools::GitLogTool;
|
||||
use crate::traits::BaseTool;
|
||||
let mut params = serde_json::json!({});
|
||||
if let Some(cwd) = args.cwd {
|
||||
params["cwd"] = serde_json::Value::String(cwd);
|
||||
}
|
||||
if let Some(count) = args.count {
|
||||
params["count"] = serde_json::Value::Number(count.into());
|
||||
}
|
||||
let result = GitLogTool
|
||||
.execute(¶ms)
|
||||
.map_err(|e| GitLogError(e.to_string()))?;
|
||||
Ok(result.content)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
//! MemoryBackendEnum — static dispatch over storage backends.
|
||||
|
||||
use super::bm25::BM25Memory;
|
||||
use super::knowledge_graph::KnowledgeGraphMemory;
|
||||
use super::sqlite::SQLiteMemory;
|
||||
use super::traits::MemoryBackend;
|
||||
use openjarvis_core::{OpenJarvisError, RetrievalResult};
|
||||
use serde_json::Value;
|
||||
|
||||
/// Closed enum of all supported memory/storage backends.
|
||||
pub enum MemoryBackendEnum {
|
||||
Sqlite(SQLiteMemory),
|
||||
Bm25(BM25Memory),
|
||||
KnowledgeGraph(KnowledgeGraphMemory),
|
||||
}
|
||||
|
||||
macro_rules! delegate_memory {
|
||||
($self:expr, $method:ident $(, $arg:expr)*) => {
|
||||
match $self {
|
||||
MemoryBackendEnum::Sqlite(m) => m.$method($($arg),*),
|
||||
MemoryBackendEnum::Bm25(m) => m.$method($($arg),*),
|
||||
MemoryBackendEnum::KnowledgeGraph(m) => m.$method($($arg),*),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl MemoryBackend for MemoryBackendEnum {
|
||||
fn backend_id(&self) -> &str {
|
||||
delegate_memory!(self, backend_id)
|
||||
}
|
||||
|
||||
fn store(
|
||||
&self,
|
||||
content: &str,
|
||||
source: &str,
|
||||
metadata: Option<&Value>,
|
||||
) -> Result<String, OpenJarvisError> {
|
||||
delegate_memory!(self, store, content, source, metadata)
|
||||
}
|
||||
|
||||
fn retrieve(
|
||||
&self,
|
||||
query: &str,
|
||||
top_k: usize,
|
||||
) -> Result<Vec<RetrievalResult>, OpenJarvisError> {
|
||||
delegate_memory!(self, retrieve, query, top_k)
|
||||
}
|
||||
|
||||
fn delete(&self, doc_id: &str) -> Result<bool, OpenJarvisError> {
|
||||
delegate_memory!(self, delete, doc_id)
|
||||
}
|
||||
|
||||
fn clear(&self) -> Result<(), OpenJarvisError> {
|
||||
delegate_memory!(self, clear)
|
||||
}
|
||||
|
||||
fn count(&self) -> Result<usize, OpenJarvisError> {
|
||||
delegate_memory!(self, count)
|
||||
}
|
||||
}
|
||||
|
||||
impl MemoryBackendEnum {
|
||||
/// Convenience: identify the backend variant key.
|
||||
pub fn variant_key(&self) -> &str {
|
||||
match self {
|
||||
MemoryBackendEnum::Sqlite(_) => "sqlite",
|
||||
MemoryBackendEnum::Bm25(_) => "bm25",
|
||||
MemoryBackendEnum::KnowledgeGraph(_) => "knowledge_graph",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
//! Memory/storage backends — SQLite FTS5, BM25, KnowledgeGraph, Hybrid.
|
||||
|
||||
pub mod backend_enum;
|
||||
pub mod bm25;
|
||||
pub mod knowledge_graph;
|
||||
pub mod sqlite;
|
||||
pub mod traits;
|
||||
pub mod utils;
|
||||
|
||||
pub use backend_enum::MemoryBackendEnum;
|
||||
pub use bm25::BM25Memory;
|
||||
pub use knowledge_graph::KnowledgeGraphMemory;
|
||||
pub use sqlite::SQLiteMemory;
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::store::TraceStore;
|
||||
use openjarvis_core::OpenJarvisError;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Debug, Clone, Default, serde::Serialize)]
|
||||
pub struct TraceStats {
|
||||
pub count: usize,
|
||||
pub success_count: usize,
|
||||
|
||||
Reference in New Issue
Block a user