Extending Penelope
Understand Penelope’s architecture and extend it with custom tools for specialized testing needs.
Architecture Overview
Core Components
PenelopeAgent
Main orchestrator coordinating test execution:
TurnExecutor
Handles individual turn execution - reasoning, tool selection, and execution.
GoalEvaluator
LLM-based evaluation of goal achievement using structured output.
Targets
Abstraction for systems under test. Penelope ships with several target adapters — pick the one that matches your system:
The MAF target preserves multi-turn context by keeping a MAF thread/session object per conversation_id, and bridges MAF’s async run() to Penelope’s synchronous target contract - so you can drive any MAF agent through Penelope’s multi-turn testing without changing the agent.
The Google ADK target uses Penelope’s conversation_id directly as the ADK session id, so multi-turn context lives where ADK already keeps it and the same key lines up in Rhesis traces. It looks a session up before creating it, which means a caller-supplied id works and a persistent session service (DatabaseSessionService, VertexAiSessionService) resumes a conversation that started in another process. ADK’s run_async is an async generator rather than a coroutine, so the target consumes the event stream and picks the assistant reply from it: the last final-response event with text, falling back to the last complete event and then to streamed partials. GoogleADKTarget accepts these constructor parameters:
| Parameter | Required | Description |
|---|---|---|
runner | Yes | An ADK Runner, or a bare ADK agent to wrap in one |
target_id | Yes | A unique identifier for the system under test |
description | No | A human-readable description; defaults to a label containing the target ID |
app_name | No | App name for session scoping; defaults to the runner’s own, else "penelope" |
user_id | No | User id passed to every ADK run call; defaults to "penelope-user" |
session_service | No | Session service used when wrapping a bare agent |
PydanticAITarget accepts these constructor parameters:
| Parameter | Required | Description |
|---|---|---|
agent | Yes | A Pydantic AI agent that provides run_sync() and run() |
target_id | Yes | A unique identifier for the system under test |
description | No | A human-readable description; defaults to a label containing the target ID |
HaystackTarget wraps either a Pipeline or an Agent, detected by shape, and replays the
conversation history to agents on each turn:
| Parameter | Required | Description |
|---|---|---|
pipeline | Yes | A Haystack Pipeline or Agent |
target_id | Yes | A unique identifier for the system under test |
description | No | A human-readable description; defaults to a label containing the wrapped type |
input_component | For pipelines | Component the message is fed to |
input_key | No | Input socket on that component. Defaults to query |
output_component | No | Component whose output holds the reply. Defaults to searching all of them |
output_key | No | Output socket holding the reply. Tried before the common names |
reply_keys | No | Replaces the list of output socket names to try |
Haystack pipelines name their components freely, so there is no single correct reply socket. The
target tries replies, last_message, messages, answers, answer, reply, result, and
output in that order, at the top level and inside each component’s output. Set output_key or
reply_keys when your pipeline puts its answer somewhere else.
Async and file-aware targets
Use a_execute_test() when Penelope runs inside an event loop. LangChain and
LangGraph targets call their native ainvoke() methods, while Pydantic AI calls
Agent.run(). This avoids blocking the event loop during target execution.
LangChain, LangGraph, and Pydantic AI targets accept file attachments as inline
dictionaries or object-storage-backed FileReference objects. HaystackTarget does not: how a
file reaches a pipeline depends on that pipeline’s own converters, so it reports an error rather
than dropping the attachment silently. Text and PDF
extractions are passed as text; other files become framework-native multimodal
content blocks. The async path fetches object-storage bytes with aread_bytes().
LangChain targets with attachments bypass string-based prompt templating so the
files remain real content blocks. Use a runnable that accepts a HumanMessage
or message list directly; extra template keyword arguments are not applied on
this path.
See Multi-modal Testing for the platform workflow
and Connector File Attachments for FileReference fields
and byte access.
Built-in Tools
Penelope includes three core tools:
- Send Message to Target - Interacts with the system under test
- Analyze Response - Evaluates target responses for goal criteria
- Extract Information - Pulls specific data from responses
Execution Flow
- Initialize - Agent receives goal, instructions, and context
- Turn Loop - For each turn up to max_turns:
- Agent reasons about current state
- Selects and executes tool
- Processes result
- Evaluates goal achievement
- Checks stopping conditions
- Completion - Returns TestResult with full history
Stopping Conditions
Tests stop when any condition is met:
Custom Tools
Extend Penelope’s capabilities by creating custom tools for specialized testing needs.
Tool Interface
All tools implement the Tool abstract base class:
Parameter Validation: Tool parameters are automatically validated via
Pydantic schemas. Your execute method receives validated inputs.
Creating a Custom Tool
Example: Database verification tool for testing data persistence.
Using Custom Tools
Writing Quality Tool Descriptions
Good descriptions help Penelope understand when and how to use your tool. Include:
- Purpose - What the tool does
- When to Use - Scenarios for using this tool
- When NOT to Use - Scenarios to avoid
- Parameters - Expected inputs with types
- Examples - Real usage examples
- Important Notes - Caveats and limitations
Multiple Custom Tools
Add multiple tools to cover more of the system in one test:
Best Practices
Clear Naming
Handle Errors Gracefully
Provide Rich Output
Test Your Tools
Real-World Examples
See complete implementations in the examples directory :
- custom_tools.py - Database verification, API monitoring, security scanning
- batch_testing.py - Batch test runner tool
- platform_integration.py - TestSet loader tool
Next steps
- See Examples for custom tools in action
- Review Configuration options