Stream-first, schema-aware AI querying. Extract structured data from LLM responses while preserving explanatory text, with automatic JSON Schema guidance and real-time streaming support.
use serde::{Deserialize, Serialize};
use schemars::JsonSchema;
use semantic_query::core::{QueryResolver, RetryConfig};
use semantic_query::clients::flexible::FlexibleClient;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
struct QuizQuestion {
/// The main question text to be asked
pub question: String,
/// Answer choice A
pub a: String,
/// Answer choice B
pub b: String,
/// Answer choice C
pub c: String,
/// Answer choice D
pub d: String,
/// The correct answer (must be exactly one of: A, B, C, or D)
#[schemars(regex(pattern = r"^[ABCD]$"))]
pub correct_answer: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
struct Quiz {
pub questions: Vec<QuizQuestion>,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load .env first so RUST_LOG in .env is seen
let _ = dotenvy::dotenv();
// Initialize tracing from RUST_LOG if provided
let _ = tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.try_init();
// Create client (env handled by FlexibleClient)
let client = FlexibleClient::claude();
let resolver = QueryResolver::new(client, RetryConfig::default());
// Get 10 science quiz questions (schema guidance is automatic)
let response = resolver.query::<Quiz>(
"Create 10 high school science quiz questions with A, B, C, D answers".to_string()
).await?;
// Extract the quiz data (new API returns ParsedResponse with mixed content)
let quiz = response.first_required()?;
// Administer the quiz
administer_quiz(quiz.questions).await;
Ok(())
}
async fn administer_quiz(questions: Vec<QuizQuestion>) {
let mut score = 0;
let total = questions.len();
for (i, question) in questions.iter().enumerate() {
println!("\nQuestion {}: {}", i + 1, question.question);
println!("A) {}", question.a);
println!("B) {}", question.b);
println!("C) {}", question.c);
println!("D) {}", question.d);
let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap();
let answer = input.trim().to_uppercase();
if answer == question.correct_answer.to_uppercase() {
score += 1;
}
}
println!("\n🎯 Quiz Complete! Final Score: {}/{} ({}%)",
score, total, (score * 100) / total);
}Setup: Add ANTHROPIC_API_KEY=your_key_here to .env file.
LLMs naturally produce mixed content - explanatory text alongside structured data. Instead of forcing everything to be JSON, we preserve both:
// Query with automatic schema guidance
let response = resolver.query::<Analysis>("Analyze this code for issues").await?;
// Access different parts of the response
let all_analyses = response.data_only(); // Vec<&Analysis> - all structured data
let full_text = response.text_content(); // String - complete text including JSON
let first = response.first_required()?; // Analysis - first item or error
// Iterate through mixed content preserving order
for item in &response.items {
match item {
ResponseItem::Text(text) => println!("Explanation: {}", text.text),
ResponseItem::Data { data, original_text } => {
println!("Found issue: {}", data.issue);
println!("Original JSON: {}", original_text);
}
}
}Stream responses token-by-token while automatically extracting structured data:
use futures_util::StreamExt;
use semantic_query::streaming::StreamItem;
let mut stream = resolver.stream_query::<ToolCall>("Help me debug this").await?;
while let Some(item) = stream.next().await {
match item? {
StreamItem::Token(tok) => print!("{}", tok), // Real-time text
StreamItem::Text(text) => { // Completed text chunk
println!("\n[Assistant] {}", text.text);
}
StreamItem::Data(tool) => { // Structured data found
println!("\n[Tool Call] {}: {:?}", tool.name, tool.args);
}
}
}QueryResolver<C: LowLevelClient>- Main API wrapper that adds schema guidance and parsingLowLevelClienttrait - Simple provider abstraction requiring onlyask_raw()methodParsedResponse<T>- Mixed content response with orderedResponseItem<T>JsonStreamProcessor- Incremental JSON parsing with structural scanning- SSE Providers - Provider-specific Server-Sent Events parsing
- Claude/Anthropic - Native streaming, automatic API key detection
- DeepSeek - Full streaming support, automatic API key detection
- ChatGPT/OpenAI - OpenAI and Azure OpenAI support with streaming
- AWS Bedrock - Feature-gated Claude support via AWS SDK
- Mock Client - Testing and development with controllable responses
// Auto-select based on available API keys
let client = FlexibleClient::auto();
// Explicit provider selection
let client = FlexibleClient::claude();
let client = FlexibleClient::deepseek();
let client = FlexibleClient::chatgpt();
// From environment detection
let client = FlexibleClient::from_type(ClientType::Claude);
// Mock for testing
let (client, handle) = FlexibleClient::mock();
handle.add_response("Mock response with JSON data");// Schema-guided mixed content extraction (recommended)
let response: ParsedResponse<T> = resolver.query::<T>(prompt).await?;
// Raw mixed content without schema guidance
let response: ParsedResponse<T> = resolver.query_mixed::<T>(prompt).await?;
// Real-time streaming with automatic JSON extraction
let mut stream = resolver.stream_query::<T>(prompt).await?;// Get all structured data items
let data_items: Vec<&T> = response.data_only();
// Get complete text content (including JSON)
let full_text: String = response.text_content();
// Get first data item (error if none)
let first: T = response.first_required()?;
// Get first data item (Option)
let first_opt: Option<&T> = response.first();
// Check if any data was found
let has_data: bool = response.has_data();
// Count data items
let count: usize = response.data_count();enum StreamItem<T> {
Token(String), // Individual tokens for real-time display
Text(TextContent), // Free-form text chunks
Data(T), // Structured data matching schema T
}Create a .env file with your API keys:
# Required for at least one provider
ANTHROPIC_API_KEY=your_anthropic_key_here
DEEPSEEK_API_KEY=your_deepseek_key_here
OPENAI_API_KEY=your_openai_key_here
# Optional Azure OpenAI configuration
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
AZURE_OPENAI_API_KEY=your_azure_key
AZURE_OPENAI_DEPLOYMENT=your-deployment-name
AZURE_OPENAI_API_VERSION=2024-02-15-preview
# Optional AWS Bedrock (requires aws-bedrock-sdk feature)
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
# Optional logging
RUST_LOG=semantic_query=debugEnable specific providers via Cargo features:
[dependencies]
semantic-query = { version = "0.2", features = ["anthropic", "deepseek"] }
# For AWS Bedrock support
semantic-query = { version = "0.2", features = ["aws-bedrock-sdk", "bedrock", "anthropic"] }Available features:
anthropic- Claude/Anthropic supportdeepseek- DeepSeek supportbedrock- AWS Bedrock support (Claude only)aws-bedrock-sdk- Full AWS SDK integration
use semantic_query::clients::{ClaudeConfig, ClaudeModel, DeepSeekConfig, DeepSeekModel};
// Custom Claude configuration
let claude_config = ClaudeConfig::anthropic(
"your_api_key".to_string(),
ClaudeModel::Sonnet35
);
let client = FlexibleClient::claude_with(claude_config);
// Custom DeepSeek configuration
let deepseek_config = DeepSeekConfig {
api_key: "your_api_key".to_string(),
model: DeepSeekModel::Coder,
max_tokens: 2048,
temperature: 0.7,
};
let client = FlexibleClient::deepseek_with(deepseek_config);use semantic_query::interceptors::FileInterceptor;
// Log all prompts and responses to files
let client = FlexibleClient::claude()
.with_file_interceptor("./query_logs".into());
// Custom interceptor implementation
struct MyInterceptor;
#[async_trait]
impl Interceptor for MyInterceptor {
async fn save(&self, prompt: &str, response: &str) -> Result<(), Box<dyn Error>> {
// Custom logging logic
Ok(())
}
}
let client = client.with_interceptor(Arc::new(MyInterceptor));match resolver.query::<MyData>(prompt).await {
Ok(response) => {
// Success - handle mixed content
}
Err(QueryResolverError::Ai(ai_error)) => {
// AI provider error (rate limit, auth, etc.)
}
Err(QueryResolverError::DataExtraction(data_error)) => {
// No structured data found in response
}
Err(QueryResolverError::JsonDeserialization(json_error, raw)) => {
// JSON parsing failed
}
Err(QueryResolverError::MaxRetriesExceeded) => {
// Retry limit reached
}
}Run the provided examples to see different features:
# Main demo with quiz generation
cargo run --example readme_demo
# Streaming demo with real-time output
cargo run --example readme_demo_streaming
# Mixed content demo showing V2 improvements
cargo run --example resolver_v2_demo
# Schema validation demo
cargo run --example schema_demo
# Benchmark tool (tests all providers)
cargo run --bin benchmark
# DeepSeek live tests (requires network + key)
cargo test --test deepseek_live -- --ignored --nocaptureThe library automatically converts your struct's doc comments and constraints into JSON schema and includes them in the AI prompt:
#[derive(Debug, Deserialize, JsonSchema)]
struct UserProfile {
/// The user's full name
pub name: String,
/// Email address must be valid format
#[schemars(regex(pattern = r"^[^@]+@[^@]+\.[^@]+$"))]
pub email: String,
/// Age must be between 0 and 150
#[schemars(minimum = 0, maximum = 150)]
pub age: u32,
/// User's interests (max 10 items)
#[schemars(max_items = 10)]
pub interests: Vec<String>,
}This ensures the AI understands exactly what each field represents and enforces constraints.
The old single-item APIs are deprecated. Migrate as follows:
// Old API (deprecated)
let result: T = resolver.query_with_schema::<T>(prompt).await?;
// New API - Option 1: Simple migration with first_required()
let result: T = resolver.query::<T>(prompt).await?.first_required()?;
// New API - Option 2: Handle multiple results
let response = resolver.query::<T>(prompt).await?;
for item in response.data_only() {
process_item(item);
}
// New API - Option 3: Access mixed content
let response = resolver.query::<T>(prompt).await?;
println!("Full explanation: {}", response.text_content());
println!("Found {} data items", response.data_count());# Run all tests
cargo test
# Run specific test modules
cargo test --test stream_parser_tests
cargo test --test sse_aggregator_tests
cargo test --test json_extract_tests
# Run with logging
RUST_LOG=semantic_query=debug cargo test -- --nocapture# DeepSeek live tests (ignored by default)
DEEPSEEK_API_KEY=your_key cargo test --test deepseek_live -- --ignored --nocapture
# Provider comparison tests
cargo test --test provider_comparison_rstest -- --nocapture# Rustc warnings
cargo check --all-targets --examples
# Strict rustc
RUSTFLAGS='-D warnings -W unused_braces' cargo check --all-targets --examples
# Clippy (recommended)
cargo clippy --all-targets --all-features -- -W clippy::all -W clippy::nursery -W clippy::pedantic -W rust-2018-idioms -W warnings# Default features (anthropic + deepseek)
cargo build
# With AWS Bedrock support
cargo build --features aws-bedrock-sdk,bedrock,anthropic
# All providers
cargo build --features anthropic,deepseek,bedrock,aws-bedrock-sdk
# Minimal build (mock only)
cargo build --no-default-featuresThis project uses a modular architecture:
- Core (
src/core.rs) - Main QueryResolver API and response types - Clients (
src/clients/) - Provider implementations (Claude, DeepSeek, ChatGPT) - Streaming (
src/streaming/) - SSE parsing and JSON extraction - JSON Utilities (
src/json_utils.rs) - Structural JSON scanning - Interceptors (
src/interceptors/) - Request/response logging
When adding new providers, implement the LowLevelClient trait and add corresponding SSE provider if streaming is supported.