refactor(cli)!: rename all skills and agents to consistent ce- prefix (#503)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# =============================================================================
|
||||
# DSPy.rb Configuration Template — v0.34.3 API
|
||||
#
|
||||
# Rails initializer patterns for DSPy.rb with RubyLLM, observability,
|
||||
# and feature-flagged model selection.
|
||||
#
|
||||
# Key patterns:
|
||||
# - Use after_initialize for Rails setup
|
||||
# - Use dspy-ruby_llm for multi-provider routing
|
||||
# - Use structured_outputs: true for reliable parsing
|
||||
# - Use dspy-o11y + dspy-o11y-langfuse for observability
|
||||
# - Use ENV-based feature flags for model selection
|
||||
# =============================================================================
|
||||
|
||||
# =============================================================================
|
||||
# Gemfile Dependencies
|
||||
# =============================================================================
|
||||
#
|
||||
# # Core
|
||||
# gem 'dspy'
|
||||
#
|
||||
# # Provider adapter (choose one strategy):
|
||||
#
|
||||
# # Strategy A: Unified adapter via RubyLLM (recommended)
|
||||
# gem 'dspy-ruby_llm'
|
||||
# gem 'ruby_llm'
|
||||
#
|
||||
# # Strategy B: Per-provider adapters (direct SDK access)
|
||||
# gem 'dspy-openai' # OpenAI, OpenRouter, Ollama
|
||||
# gem 'dspy-anthropic' # Claude
|
||||
# gem 'dspy-gemini' # Gemini
|
||||
#
|
||||
# # Observability (optional)
|
||||
# gem 'dspy-o11y'
|
||||
# gem 'dspy-o11y-langfuse'
|
||||
#
|
||||
# # Optimization (optional)
|
||||
# gem 'dspy-miprov2' # MIPROv2 optimizer
|
||||
# gem 'dspy-gepa' # GEPA optimizer
|
||||
#
|
||||
# # Schema formats (optional)
|
||||
# gem 'sorbet-baml' # BAML schema format (84% token reduction)
|
||||
|
||||
# =============================================================================
|
||||
# Rails Initializer — config/initializers/dspy.rb
|
||||
# =============================================================================
|
||||
|
||||
Rails.application.config.after_initialize do
|
||||
# Skip in test unless explicitly enabled
|
||||
next if Rails.env.test? && ENV["DSPY_ENABLE_IN_TEST"].blank?
|
||||
|
||||
# Configure RubyLLM provider credentials
|
||||
RubyLLM.configure do |config|
|
||||
config.gemini_api_key = ENV["GEMINI_API_KEY"] if ENV["GEMINI_API_KEY"].present?
|
||||
config.anthropic_api_key = ENV["ANTHROPIC_API_KEY"] if ENV["ANTHROPIC_API_KEY"].present?
|
||||
config.openai_api_key = ENV["OPENAI_API_KEY"] if ENV["OPENAI_API_KEY"].present?
|
||||
end
|
||||
|
||||
# Configure DSPy with unified RubyLLM adapter
|
||||
model = ENV.fetch("DSPY_MODEL", "ruby_llm/gemini-2.5-flash")
|
||||
DSPy.configure do |config|
|
||||
config.lm = DSPy::LM.new(model, structured_outputs: true)
|
||||
config.logger = Rails.logger
|
||||
end
|
||||
|
||||
# Enable Langfuse observability (optional)
|
||||
if ENV["LANGFUSE_PUBLIC_KEY"].present? && ENV["LANGFUSE_SECRET_KEY"].present?
|
||||
DSPy::Observability.configure!
|
||||
end
|
||||
end
|
||||
|
||||
# =============================================================================
|
||||
# Feature Flags — config/initializers/feature_flags.rb
|
||||
# =============================================================================
|
||||
|
||||
# Use different models for different roles:
|
||||
# - Fast/cheap for classification, routing, simple tasks
|
||||
# - Powerful for synthesis, reasoning, complex analysis
|
||||
|
||||
module FeatureFlags
|
||||
SELECTOR_MODEL = ENV.fetch("DSPY_SELECTOR_MODEL", "ruby_llm/gemini-2.5-flash-lite")
|
||||
SYNTHESIZER_MODEL = ENV.fetch("DSPY_SYNTHESIZER_MODEL", "ruby_llm/gemini-2.5-flash")
|
||||
REASONING_MODEL = ENV.fetch("DSPY_REASONING_MODEL", "ruby_llm/claude-sonnet-4-20250514")
|
||||
end
|
||||
|
||||
# Usage in tools/modules:
|
||||
#
|
||||
# class ClassifyTool < DSPy::Tools::Base
|
||||
# def call(query:)
|
||||
# predictor = DSPy::Predict.new(ClassifySignature)
|
||||
# predictor.configure { |c| c.lm = DSPy::LM.new(FeatureFlags::SELECTOR_MODEL, structured_outputs: true) }
|
||||
# predictor.call(query: query)
|
||||
# end
|
||||
# end
|
||||
|
||||
# =============================================================================
|
||||
# Environment Variables — .env
|
||||
# =============================================================================
|
||||
#
|
||||
# # Provider API keys (set the ones you need)
|
||||
# GEMINI_API_KEY=...
|
||||
# ANTHROPIC_API_KEY=...
|
||||
# OPENAI_API_KEY=...
|
||||
#
|
||||
# # DSPy model configuration
|
||||
# DSPY_MODEL=ruby_llm/gemini-2.5-flash
|
||||
# DSPY_SELECTOR_MODEL=ruby_llm/gemini-2.5-flash-lite
|
||||
# DSPY_SYNTHESIZER_MODEL=ruby_llm/gemini-2.5-flash
|
||||
# DSPY_REASONING_MODEL=ruby_llm/claude-sonnet-4-20250514
|
||||
#
|
||||
# # Langfuse observability (optional)
|
||||
# LANGFUSE_PUBLIC_KEY=pk-...
|
||||
# LANGFUSE_SECRET_KEY=sk-...
|
||||
# DSPY_TELEMETRY_BATCH_SIZE=5
|
||||
#
|
||||
# # Test environment
|
||||
# DSPY_ENABLE_IN_TEST=1 # Set to enable DSPy in test env
|
||||
|
||||
# =============================================================================
|
||||
# Per-Provider Configuration (without RubyLLM)
|
||||
# =============================================================================
|
||||
|
||||
# OpenAI (dspy-openai gem)
|
||||
# DSPy.configure do |c|
|
||||
# c.lm = DSPy::LM.new('openai/gpt-4o-mini', api_key: ENV['OPENAI_API_KEY'])
|
||||
# end
|
||||
|
||||
# Anthropic (dspy-anthropic gem)
|
||||
# DSPy.configure do |c|
|
||||
# c.lm = DSPy::LM.new('anthropic/claude-sonnet-4-20250514', api_key: ENV['ANTHROPIC_API_KEY'])
|
||||
# end
|
||||
|
||||
# Gemini (dspy-gemini gem)
|
||||
# DSPy.configure do |c|
|
||||
# c.lm = DSPy::LM.new('gemini/gemini-2.5-flash', api_key: ENV['GEMINI_API_KEY'])
|
||||
# end
|
||||
|
||||
# Ollama (dspy-openai gem, local models)
|
||||
# DSPy.configure do |c|
|
||||
# c.lm = DSPy::LM.new('ollama/llama3.2', base_url: 'http://localhost:11434')
|
||||
# end
|
||||
|
||||
# OpenRouter (dspy-openai gem, 200+ models)
|
||||
# DSPy.configure do |c|
|
||||
# c.lm = DSPy::LM.new('openrouter/anthropic/claude-3.5-sonnet',
|
||||
# api_key: ENV['OPENROUTER_API_KEY'],
|
||||
# base_url: 'https://openrouter.ai/api/v1')
|
||||
# end
|
||||
|
||||
# =============================================================================
|
||||
# VCR Test Configuration — spec/support/dspy.rb
|
||||
# =============================================================================
|
||||
|
||||
# VCR.configure do |config|
|
||||
# config.cassette_library_dir = "spec/vcr_cassettes"
|
||||
# config.hook_into :webmock
|
||||
# config.configure_rspec_metadata!
|
||||
# config.filter_sensitive_data('<GEMINI_API_KEY>') { ENV['GEMINI_API_KEY'] }
|
||||
# config.filter_sensitive_data('<OPENAI_API_KEY>') { ENV['OPENAI_API_KEY'] }
|
||||
# config.filter_sensitive_data('<ANTHROPIC_API_KEY>') { ENV['ANTHROPIC_API_KEY'] }
|
||||
# end
|
||||
|
||||
# =============================================================================
|
||||
# Schema Format Configuration (optional)
|
||||
# =============================================================================
|
||||
|
||||
# BAML schema format — 84% token reduction for Enhanced Prompting mode
|
||||
# DSPy.configure do |c|
|
||||
# c.lm = DSPy::LM.new('openai/gpt-4o-mini',
|
||||
# api_key: ENV['OPENAI_API_KEY'],
|
||||
# schema_format: :baml # Requires sorbet-baml gem
|
||||
# )
|
||||
# end
|
||||
|
||||
# TOON schema + data format — table-oriented format
|
||||
# DSPy.configure do |c|
|
||||
# c.lm = DSPy::LM.new('openai/gpt-4o-mini',
|
||||
# api_key: ENV['OPENAI_API_KEY'],
|
||||
# schema_format: :toon, # How DSPy describes the signature
|
||||
# data_format: :toon # How inputs/outputs are rendered in prompts
|
||||
# )
|
||||
# end
|
||||
#
|
||||
# Note: BAML and TOON apply only when structured_outputs: false.
|
||||
# With structured_outputs: true, the provider receives JSON Schema directly.
|
||||
@@ -0,0 +1,300 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# =============================================================================
|
||||
# DSPy.rb Module Template — v0.34.3 API
|
||||
#
|
||||
# Modules orchestrate predictors, tools, and business logic.
|
||||
#
|
||||
# Key patterns:
|
||||
# - Use .call() to invoke (not .forward())
|
||||
# - Access results with result.field (not result[:field])
|
||||
# - Use DSPy::Tools::Base for tools (not DSPy::Tool)
|
||||
# - Use lifecycle callbacks (before/around/after) for cross-cutting concerns
|
||||
# - Use DSPy.with_lm for temporary model overrides
|
||||
# - Use configure_predictor for fine-grained agent control
|
||||
# =============================================================================
|
||||
|
||||
# --- Basic Module ---
|
||||
|
||||
class BasicClassifier < DSPy::Module
|
||||
def initialize
|
||||
super
|
||||
@predictor = DSPy::Predict.new(ClassificationSignature)
|
||||
end
|
||||
|
||||
def forward(text:)
|
||||
@predictor.call(text: text)
|
||||
end
|
||||
end
|
||||
|
||||
# Usage:
|
||||
# classifier = BasicClassifier.new
|
||||
# result = classifier.call(text: "This is a test")
|
||||
# result.category # => "technical"
|
||||
# result.confidence # => 0.95
|
||||
|
||||
# --- Module with Chain of Thought ---
|
||||
|
||||
class ReasoningClassifier < DSPy::Module
|
||||
def initialize
|
||||
super
|
||||
@predictor = DSPy::ChainOfThought.new(ClassificationSignature)
|
||||
end
|
||||
|
||||
def forward(text:)
|
||||
result = @predictor.call(text: text)
|
||||
# ChainOfThought adds result.reasoning automatically
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
# --- Module with Lifecycle Callbacks ---
|
||||
|
||||
class InstrumentedModule < DSPy::Module
|
||||
before :setup_metrics
|
||||
around :manage_context
|
||||
after :log_completion
|
||||
|
||||
def initialize
|
||||
super
|
||||
@predictor = DSPy::Predict.new(AnalysisSignature)
|
||||
@start_time = nil
|
||||
end
|
||||
|
||||
def forward(query:)
|
||||
@predictor.call(query: query)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Runs before forward
|
||||
def setup_metrics
|
||||
@start_time = Time.now
|
||||
Rails.logger.info "Starting prediction"
|
||||
end
|
||||
|
||||
# Wraps forward — must call yield
|
||||
def manage_context
|
||||
load_user_context
|
||||
result = yield
|
||||
save_updated_context(result)
|
||||
result
|
||||
end
|
||||
|
||||
# Runs after forward completes
|
||||
def log_completion
|
||||
duration = Time.now - @start_time
|
||||
Rails.logger.info "Prediction completed in #{duration}s"
|
||||
end
|
||||
|
||||
def load_user_context = nil
|
||||
def save_updated_context(_result) = nil
|
||||
end
|
||||
|
||||
# Execution order: before → around (before yield) → forward → around (after yield) → after
|
||||
# Callbacks are inherited from parent classes and execute in registration order.
|
||||
|
||||
# --- Module with Tools ---
|
||||
|
||||
class SearchTool < DSPy::Tools::Base
|
||||
tool_name "search"
|
||||
tool_description "Search for information by query"
|
||||
|
||||
sig { params(query: String, max_results: Integer).returns(T::Array[T::Hash[Symbol, String]]) }
|
||||
def call(query:, max_results: 5)
|
||||
# Implementation here
|
||||
[{ title: "Result 1", url: "https://example.com" }]
|
||||
end
|
||||
end
|
||||
|
||||
class FinishTool < DSPy::Tools::Base
|
||||
tool_name "finish"
|
||||
tool_description "Submit the final answer"
|
||||
|
||||
sig { params(answer: String).returns(String) }
|
||||
def call(answer:)
|
||||
answer
|
||||
end
|
||||
end
|
||||
|
||||
class ResearchAgent < DSPy::Module
|
||||
def initialize
|
||||
super
|
||||
tools = [SearchTool.new, FinishTool.new]
|
||||
@agent = DSPy::ReAct.new(
|
||||
ResearchSignature,
|
||||
tools: tools,
|
||||
max_iterations: 5
|
||||
)
|
||||
end
|
||||
|
||||
def forward(question:)
|
||||
@agent.call(question: question)
|
||||
end
|
||||
end
|
||||
|
||||
# --- Module with Per-Task Model Selection ---
|
||||
|
||||
class SmartRouter < DSPy::Module
|
||||
def initialize
|
||||
super
|
||||
@classifier = DSPy::Predict.new(RouteSignature)
|
||||
@analyzer = DSPy::ChainOfThought.new(AnalysisSignature)
|
||||
end
|
||||
|
||||
def forward(text:)
|
||||
# Use fast model for classification
|
||||
DSPy.with_lm(fast_model) do
|
||||
route = @classifier.call(text: text)
|
||||
|
||||
if route.requires_deep_analysis
|
||||
# Switch to powerful model for analysis
|
||||
DSPy.with_lm(powerful_model) do
|
||||
@analyzer.call(text: text)
|
||||
end
|
||||
else
|
||||
route
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fast_model
|
||||
@fast_model ||= DSPy::LM.new(
|
||||
ENV.fetch("DSPY_SELECTOR_MODEL", "ruby_llm/gemini-2.5-flash-lite"),
|
||||
structured_outputs: true
|
||||
)
|
||||
end
|
||||
|
||||
def powerful_model
|
||||
@powerful_model ||= DSPy::LM.new(
|
||||
ENV.fetch("DSPY_SYNTHESIZER_MODEL", "ruby_llm/gemini-2.5-flash"),
|
||||
structured_outputs: true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# --- Module with configure_predictor ---
|
||||
|
||||
class ConfiguredAgent < DSPy::Module
|
||||
def initialize
|
||||
super
|
||||
tools = [SearchTool.new, FinishTool.new]
|
||||
@agent = DSPy::ReAct.new(ResearchSignature, tools: tools)
|
||||
|
||||
# Set default model for all internal predictors
|
||||
@agent.configure { |c| c.lm = DSPy::LM.new('ruby_llm/gemini-2.5-flash', structured_outputs: true) }
|
||||
|
||||
# Override specific predictor with a more capable model
|
||||
@agent.configure_predictor('thought_generator') do |c|
|
||||
c.lm = DSPy::LM.new('ruby_llm/claude-sonnet-4-20250514', structured_outputs: true)
|
||||
end
|
||||
end
|
||||
|
||||
def forward(question:)
|
||||
@agent.call(question: question)
|
||||
end
|
||||
end
|
||||
|
||||
# Available internal predictors by agent type:
|
||||
# DSPy::ReAct → thought_generator, observation_processor
|
||||
# DSPy::CodeAct → code_generator, observation_processor
|
||||
# DSPy::DeepSearch → seed_predictor, search_predictor, reader_predictor, reason_predictor
|
||||
|
||||
# --- Module with Event Subscriptions ---
|
||||
|
||||
class TokenTrackingModule < DSPy::Module
|
||||
subscribe 'lm.tokens', :track_tokens, scope: :descendants
|
||||
|
||||
def initialize
|
||||
super
|
||||
@predictor = DSPy::Predict.new(AnalysisSignature)
|
||||
@total_tokens = 0
|
||||
end
|
||||
|
||||
def forward(query:)
|
||||
@predictor.call(query: query)
|
||||
end
|
||||
|
||||
def track_tokens(_event, attrs)
|
||||
@total_tokens += attrs.fetch(:total_tokens, 0)
|
||||
end
|
||||
|
||||
def token_usage
|
||||
@total_tokens
|
||||
end
|
||||
end
|
||||
|
||||
# Module-scoped subscriptions automatically scope to the module instance and descendants.
|
||||
# Use scope: :self_only to restrict delivery to the module itself (ignoring children).
|
||||
|
||||
# --- Tool That Wraps a Prediction ---
|
||||
|
||||
class RerankTool < DSPy::Tools::Base
|
||||
tool_name "rerank"
|
||||
tool_description "Score and rank search results by relevance"
|
||||
|
||||
MAX_ITEMS = 200
|
||||
MIN_ITEMS_FOR_LLM = 5
|
||||
|
||||
sig { params(query: String, items: T::Array[T::Hash[Symbol, T.untyped]]).returns(T::Hash[Symbol, T.untyped]) }
|
||||
def call(query:, items: [])
|
||||
# Short-circuit: skip LLM for small sets
|
||||
return { scored_items: items, reranked: false } if items.size < MIN_ITEMS_FOR_LLM
|
||||
|
||||
# Cap to prevent token overflow
|
||||
capped_items = items.first(MAX_ITEMS)
|
||||
|
||||
predictor = DSPy::Predict.new(RerankSignature)
|
||||
predictor.configure { |c| c.lm = DSPy::LM.new("ruby_llm/gemini-2.5-flash", structured_outputs: true) }
|
||||
|
||||
result = predictor.call(query: query, items: capped_items)
|
||||
{ scored_items: result.scored_items, reranked: true }
|
||||
rescue => e
|
||||
Rails.logger.warn "[RerankTool] LLM rerank failed: #{e.message}"
|
||||
{ error: "Rerank failed: #{e.message}", scored_items: items, reranked: false }
|
||||
end
|
||||
end
|
||||
|
||||
# Key patterns for tools wrapping predictions:
|
||||
# - Short-circuit LLM calls when unnecessary (small data, trivial cases)
|
||||
# - Cap input size to prevent token overflow
|
||||
# - Per-tool model selection via configure
|
||||
# - Graceful error handling with fallback data
|
||||
|
||||
# --- Multi-Step Pipeline ---
|
||||
|
||||
class AnalysisPipeline < DSPy::Module
|
||||
def initialize
|
||||
super
|
||||
@classifier = DSPy::Predict.new(ClassifySignature)
|
||||
@analyzer = DSPy::ChainOfThought.new(AnalyzeSignature)
|
||||
@summarizer = DSPy::Predict.new(SummarizeSignature)
|
||||
end
|
||||
|
||||
def forward(text:)
|
||||
classification = @classifier.call(text: text)
|
||||
analysis = @analyzer.call(text: text, category: classification.category)
|
||||
@summarizer.call(analysis: analysis.reasoning, category: classification.category)
|
||||
end
|
||||
end
|
||||
|
||||
# --- Observability with Spans ---
|
||||
|
||||
class TracedModule < DSPy::Module
|
||||
def initialize
|
||||
super
|
||||
@predictor = DSPy::Predict.new(AnalysisSignature)
|
||||
end
|
||||
|
||||
def forward(query:)
|
||||
DSPy::Context.with_span(
|
||||
operation: "traced_module.analyze",
|
||||
"dspy.module" => self.class.name,
|
||||
"query.length" => query.length.to_s
|
||||
) do
|
||||
@predictor.call(query: query)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,221 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# =============================================================================
|
||||
# DSPy.rb Signature Template — v0.34.3 API
|
||||
#
|
||||
# Signatures define the interface between your application and LLMs.
|
||||
# They specify inputs, outputs, and task descriptions using Sorbet types.
|
||||
#
|
||||
# Key patterns:
|
||||
# - Use T::Enum classes for controlled outputs (not inline T.enum([...]))
|
||||
# - Use description: kwarg on fields to guide the LLM
|
||||
# - Use default values for optional fields
|
||||
# - Use Date/DateTime/Time for temporal data (auto-converted)
|
||||
# - Access results with result.field (not result[:field])
|
||||
# - Invoke with predictor.call() (not predictor.forward())
|
||||
# =============================================================================
|
||||
|
||||
# --- Basic Signature ---
|
||||
|
||||
class SentimentAnalysis < DSPy::Signature
|
||||
description "Analyze sentiment of text"
|
||||
|
||||
class Sentiment < T::Enum
|
||||
enums do
|
||||
Positive = new('positive')
|
||||
Negative = new('negative')
|
||||
Neutral = new('neutral')
|
||||
end
|
||||
end
|
||||
|
||||
input do
|
||||
const :text, String
|
||||
end
|
||||
|
||||
output do
|
||||
const :sentiment, Sentiment
|
||||
const :score, Float, description: "Confidence score from 0.0 to 1.0"
|
||||
end
|
||||
end
|
||||
|
||||
# Usage:
|
||||
# predictor = DSPy::Predict.new(SentimentAnalysis)
|
||||
# result = predictor.call(text: "This product is amazing!")
|
||||
# result.sentiment # => Sentiment::Positive
|
||||
# result.score # => 0.92
|
||||
|
||||
# --- Signature with Date/Time Types ---
|
||||
|
||||
class EventScheduler < DSPy::Signature
|
||||
description "Schedule events based on requirements"
|
||||
|
||||
input do
|
||||
const :event_name, String
|
||||
const :start_date, Date # ISO 8601: YYYY-MM-DD
|
||||
const :end_date, T.nilable(Date) # Optional date
|
||||
const :preferred_time, DateTime # ISO 8601 with timezone
|
||||
const :deadline, Time # Stored as UTC
|
||||
end
|
||||
|
||||
output do
|
||||
const :scheduled_date, Date # LLM returns ISO string, auto-converted
|
||||
const :event_datetime, DateTime # Preserves timezone
|
||||
const :created_at, Time # Converted to UTC
|
||||
end
|
||||
end
|
||||
|
||||
# Date/Time format handling:
|
||||
# Date → ISO 8601 (YYYY-MM-DD)
|
||||
# DateTime → ISO 8601 with timezone (YYYY-MM-DDTHH:MM:SS+00:00)
|
||||
# Time → ISO 8601, automatically converted to UTC
|
||||
|
||||
# --- Signature with Default Values ---
|
||||
|
||||
class SmartSearch < DSPy::Signature
|
||||
description "Search with intelligent defaults"
|
||||
|
||||
input do
|
||||
const :query, String
|
||||
const :max_results, Integer, default: 10
|
||||
const :language, String, default: "English"
|
||||
const :include_metadata, T::Boolean, default: false
|
||||
end
|
||||
|
||||
output do
|
||||
const :results, T::Array[String]
|
||||
const :total_found, Integer
|
||||
const :search_time_ms, Float, default: 0.0 # Fallback if LLM omits
|
||||
const :cached, T::Boolean, default: false
|
||||
end
|
||||
end
|
||||
|
||||
# Input defaults reduce boilerplate:
|
||||
# search = DSPy::Predict.new(SmartSearch)
|
||||
# result = search.call(query: "Ruby programming")
|
||||
# # max_results=10, language="English", include_metadata=false are applied
|
||||
|
||||
# --- Signature with Nested Structs and Field Descriptions ---
|
||||
|
||||
class EntityExtraction < DSPy::Signature
|
||||
description "Extract named entities from text"
|
||||
|
||||
class EntityType < T::Enum
|
||||
enums do
|
||||
Person = new('person')
|
||||
Organization = new('organization')
|
||||
Location = new('location')
|
||||
DateEntity = new('date')
|
||||
end
|
||||
end
|
||||
|
||||
class Entity < T::Struct
|
||||
const :name, String, description: "The entity text as it appears in the source"
|
||||
const :type, EntityType
|
||||
const :confidence, Float, description: "Extraction confidence from 0.0 to 1.0"
|
||||
const :start_offset, Integer, default: 0
|
||||
end
|
||||
|
||||
input do
|
||||
const :text, String
|
||||
const :entity_types, T::Array[EntityType], default: [],
|
||||
description: "Filter to these entity types; empty means all types"
|
||||
end
|
||||
|
||||
output do
|
||||
const :entities, T::Array[Entity]
|
||||
const :total_found, Integer
|
||||
end
|
||||
end
|
||||
|
||||
# --- Signature with Union Types ---
|
||||
|
||||
class FlexibleClassification < DSPy::Signature
|
||||
description "Classify input with flexible result type"
|
||||
|
||||
class Category < T::Enum
|
||||
enums do
|
||||
Technical = new('technical')
|
||||
Business = new('business')
|
||||
Personal = new('personal')
|
||||
end
|
||||
end
|
||||
|
||||
input do
|
||||
const :text, String
|
||||
end
|
||||
|
||||
output do
|
||||
const :category, Category
|
||||
const :result, T.any(Float, String),
|
||||
description: "Numeric score or text explanation depending on classification"
|
||||
const :confidence, Float
|
||||
end
|
||||
end
|
||||
|
||||
# --- Signature with Recursive Types ---
|
||||
|
||||
class DocumentParser < DSPy::Signature
|
||||
description "Parse document into tree structure"
|
||||
|
||||
class NodeType < T::Enum
|
||||
enums do
|
||||
Heading = new('heading')
|
||||
Paragraph = new('paragraph')
|
||||
List = new('list')
|
||||
CodeBlock = new('code_block')
|
||||
end
|
||||
end
|
||||
|
||||
class TreeNode < T::Struct
|
||||
const :node_type, NodeType, description: "The type of document element"
|
||||
const :text, String, default: "", description: "Text content of the node"
|
||||
const :level, Integer, default: 0
|
||||
const :children, T::Array[TreeNode], default: [] # Self-reference → $defs in JSON Schema
|
||||
end
|
||||
|
||||
input do
|
||||
const :html, String, description: "Raw HTML to parse"
|
||||
end
|
||||
|
||||
output do
|
||||
const :root, TreeNode
|
||||
const :word_count, Integer
|
||||
end
|
||||
end
|
||||
|
||||
# The schema generator creates #/$defs/TreeNode references for recursive types,
|
||||
# compatible with OpenAI and Gemini structured outputs.
|
||||
# Use `default: []` instead of `T.nilable(T::Array[...])` for OpenAI compatibility.
|
||||
|
||||
# --- Vision Signature ---
|
||||
|
||||
class ImageAnalysis < DSPy::Signature
|
||||
description "Analyze an image and answer questions about its content"
|
||||
|
||||
input do
|
||||
const :image, DSPy::Image, description: "The image to analyze"
|
||||
const :question, String, description: "Question about the image content"
|
||||
end
|
||||
|
||||
output do
|
||||
const :answer, String
|
||||
const :confidence, Float, description: "Confidence in the answer (0.0-1.0)"
|
||||
end
|
||||
end
|
||||
|
||||
# Vision usage:
|
||||
# predictor = DSPy::Predict.new(ImageAnalysis)
|
||||
# result = predictor.call(
|
||||
# image: DSPy::Image.from_file("path/to/image.jpg"),
|
||||
# question: "What objects are visible?"
|
||||
# )
|
||||
# result.answer # => "The image shows..."
|
||||
|
||||
# --- Accessing Schemas Programmatically ---
|
||||
#
|
||||
# SentimentAnalysis.input_json_schema # => { type: "object", properties: { ... } }
|
||||
# SentimentAnalysis.output_json_schema # => { type: "object", properties: { ... } }
|
||||
#
|
||||
# # Field descriptions propagate to JSON Schema
|
||||
# Entity.field_descriptions[:name] # => "The entity text as it appears in the source"
|
||||
# Entity.field_descriptions[:confidence] # => "Extraction confidence from 0.0 to 1.0"
|
||||
Reference in New Issue
Block a user