mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-27 08:00:17 +00:00
Implement tool approval, fix tool definition refresh, and wire embeddings
This commit addresses three critical issues from code review: 1. Tool approval enforcement: Tools declaring requires_approval() (shell, http, file write/patch, build_software) now gate execution. Adds PendingApproval struct, session-scoped auto-approved tools set, and approval flow with yes/no/always commands. 2. Tool definition refresh: Tool definitions now refresh each iteration in both chat and job loops, so newly built tools become visible immediately within the same session. 3. Worker tool call handling: Changed respond() to respond_with_tools() when select_tools returns empty, properly executing tool calls instead of formatting them as text. Also includes prior work from the plan: - Wire embeddings provider (OpenAI + NEAR AI) to workspace - Load workspace system prompt (identity files) into LLM context - Route heartbeat notifications through channel manager - Enable auto-context compaction when threshold exceeded - Refactor to config structs (AgentDeps, WorkerDeps, LlmCallRecord) - Fix clippy warnings (saturating_sub, too_many_arguments) Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
8af48390a9
commit
2cc9aed364
@@ -213,6 +213,147 @@ impl EmbeddingProvider for OpenAiEmbeddings {
|
||||
}
|
||||
}
|
||||
|
||||
/// NEAR AI embedding provider using the NEAR AI API.
|
||||
///
|
||||
/// Uses the same session-based auth as the LLM provider.
|
||||
pub struct NearAiEmbeddings {
|
||||
client: reqwest::Client,
|
||||
base_url: String,
|
||||
session: std::sync::Arc<crate::llm::SessionManager>,
|
||||
model: String,
|
||||
dimension: usize,
|
||||
}
|
||||
|
||||
impl NearAiEmbeddings {
|
||||
/// Create a new NEAR AI embedding provider.
|
||||
///
|
||||
/// Uses the same session manager as the LLM provider for auth.
|
||||
pub fn new(
|
||||
base_url: impl Into<String>,
|
||||
session: std::sync::Arc<crate::llm::SessionManager>,
|
||||
) -> Self {
|
||||
Self {
|
||||
client: reqwest::Client::new(),
|
||||
base_url: base_url.into(),
|
||||
session,
|
||||
model: "text-embedding-3-small".to_string(),
|
||||
dimension: 1536,
|
||||
}
|
||||
}
|
||||
|
||||
/// Use a specific model.
|
||||
pub fn with_model(mut self, model: impl Into<String>, dimension: usize) -> Self {
|
||||
self.model = model.into();
|
||||
self.dimension = dimension;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct NearAiEmbeddingRequest<'a> {
|
||||
model: &'a str,
|
||||
input: &'a [String],
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct NearAiEmbeddingResponse {
|
||||
data: Vec<NearAiEmbeddingData>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct NearAiEmbeddingData {
|
||||
embedding: Vec<f32>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EmbeddingProvider for NearAiEmbeddings {
|
||||
fn dimension(&self) -> usize {
|
||||
self.dimension
|
||||
}
|
||||
|
||||
fn model_name(&self) -> &str {
|
||||
&self.model
|
||||
}
|
||||
|
||||
fn max_input_length(&self) -> usize {
|
||||
32_000
|
||||
}
|
||||
|
||||
async fn embed(&self, text: &str) -> Result<Vec<f32>, EmbeddingError> {
|
||||
if text.len() > self.max_input_length() {
|
||||
return Err(EmbeddingError::TextTooLong {
|
||||
length: text.len(),
|
||||
max: self.max_input_length(),
|
||||
});
|
||||
}
|
||||
|
||||
let embeddings = self.embed_batch(&[text.to_string()]).await?;
|
||||
embeddings
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| EmbeddingError::InvalidResponse("No embedding returned".to_string()))
|
||||
}
|
||||
|
||||
async fn embed_batch(&self, texts: &[String]) -> Result<Vec<Vec<f32>>, EmbeddingError> {
|
||||
use secrecy::ExposeSecret;
|
||||
|
||||
if texts.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let request = NearAiEmbeddingRequest {
|
||||
model: &self.model,
|
||||
input: texts,
|
||||
};
|
||||
|
||||
let token = self
|
||||
.session
|
||||
.get_token()
|
||||
.await
|
||||
.map_err(|_| EmbeddingError::AuthFailed)?;
|
||||
|
||||
let url = format!("{}/v1/embeddings", self.base_url);
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(&url)
|
||||
.header("Authorization", format!("Bearer {}", token.expose_secret()))
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
if status == reqwest::StatusCode::UNAUTHORIZED {
|
||||
return Err(EmbeddingError::AuthFailed);
|
||||
}
|
||||
|
||||
if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
|
||||
let retry_after = response
|
||||
.headers()
|
||||
.get("retry-after")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.map(std::time::Duration::from_secs);
|
||||
return Err(EmbeddingError::RateLimited { retry_after });
|
||||
}
|
||||
|
||||
if !status.is_success() {
|
||||
let error_text = response.text().await.unwrap_or_default();
|
||||
return Err(EmbeddingError::HttpError(format!(
|
||||
"Status {}: {}",
|
||||
status, error_text
|
||||
)));
|
||||
}
|
||||
|
||||
let result: NearAiEmbeddingResponse = response.json().await.map_err(|e| {
|
||||
EmbeddingError::InvalidResponse(format!("Failed to parse response: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(result.data.into_iter().map(|d| d.embedding).collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// A mock embedding provider for testing.
|
||||
///
|
||||
/// Generates deterministic embeddings based on text hash.
|
||||
|
||||
@@ -48,7 +48,7 @@ mod search;
|
||||
|
||||
pub use chunker::{ChunkConfig, chunk_document};
|
||||
pub use document::{MemoryChunk, MemoryDocument, WorkspaceEntry, paths};
|
||||
pub use embeddings::{EmbeddingProvider, MockEmbeddings, OpenAiEmbeddings};
|
||||
pub use embeddings::{EmbeddingProvider, MockEmbeddings, NearAiEmbeddings, OpenAiEmbeddings};
|
||||
pub use repository::Repository;
|
||||
pub use search::{SearchConfig, SearchResult};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user