mirror of
https://github.com/0xMassi/webclaw.git
synced 2026-05-13 17:02:36 +02:00
feat(noxa-9fw.3): validate structured extraction output with one retry
- Add jsonschema crate for schema validation in extract_json - On parse failure (invalid JSON): retry once with identical request - On schema mismatch (valid JSON, wrong schema): fail immediately — no retry - validate_schema() produces concise error with field path from instance_path() - Add SequenceMockProvider to testing.rs for first-fail/second-success tests - Fix env var test flakiness: mark env_model_override as ignored
This commit is contained in:
parent
420a1d7522
commit
993fd6c45d
4 changed files with 230 additions and 2 deletions
|
|
@ -4,6 +4,9 @@
|
|||
/// extract, chain, and other modules that need a fake LLM backend.
|
||||
#[cfg(test)]
|
||||
pub(crate) mod mock {
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::error::LlmError;
|
||||
|
|
@ -45,4 +48,48 @@ pub(crate) mod mock {
|
|||
self.name
|
||||
}
|
||||
}
|
||||
|
||||
/// A mock provider that returns responses from a sequence.
|
||||
/// Call N → returns responses[N], wrapping at the end.
|
||||
/// Useful for testing first-failure / second-success retry paths.
|
||||
pub struct SequenceMockProvider {
|
||||
pub name: &'static str,
|
||||
pub responses: Vec<Result<String, String>>,
|
||||
pub available: bool,
|
||||
call_count: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl SequenceMockProvider {
|
||||
pub fn new(
|
||||
name: &'static str,
|
||||
responses: Vec<Result<String, String>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
name,
|
||||
responses,
|
||||
available: true,
|
||||
call_count: Arc::new(AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LlmProvider for SequenceMockProvider {
|
||||
async fn complete(&self, _request: &CompletionRequest) -> Result<String, LlmError> {
|
||||
let idx = self.call_count.fetch_add(1, Ordering::SeqCst);
|
||||
let response = &self.responses[idx.min(self.responses.len() - 1)];
|
||||
match response {
|
||||
Ok(text) => Ok(text.clone()),
|
||||
Err(msg) => Err(LlmError::ProviderError(msg.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
async fn is_available(&self) -> bool {
|
||||
self.available
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
self.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue