How to Create Custom Agents Using the OpenDerisk Agent Development Framework
You create custom agents in OpenDerisk by subclassing ConversableAgent (for single agents) or ManagerAgent (for teams), defining a ProfileConfig for identity and prompts, and registering Action classes via self._init_actions() to expose executable tools.
The OpenDerisk agent development framework, available in the derisk-ai/openderisk repository, provides a modular foundation for building task-oriented AI agents. When you create custom agents using this framework, you leverage pre-built abstractions for message routing, memory persistence, LLM interaction, and parallel action execution while only implementing your specific business logic. This guide demonstrates the exact patterns used in production within the packages/derisk-core/src/derisk/agent/ directory.
Core Abstractions of the OpenDerisk Agent Framework
Before writing code, understand the five core classes you will combine to create custom agents:
ConversableAgent(base_agent.py): The base class that implements the complete send/receive/reply loop, GPT-S memory handling, and LLM client interaction. Subclass this for single-agent implementations.ManagerAgent(base_team.py): ExtendsConversableAgentwith team-management capabilities for orchestrating multiple sub-agents and parallel actions.ProfileConfig(profile/base.py): Declares the agent's name, role, goal, system prompts, and constraints that are injected into the LLM context.Action(actions/agent_action.py): Encapsulates a single tool or skill that an agent can invoke. Actions are registered in the agent's_init_actionslist during initialization.VariableManager(variable.py): Provides dynamic variable injection (e.g., available tools, knowledge bases) into prompt templates at runtime.
Step-by-Step Guide to Create Custom Agents
Follow these implementation steps used throughout the OpenDerisk codebase:
- Create a subclass of
ConversableAgent(for single agents) orManagerAgent(for multi-agent teams). - Define a
ProfileConfigclass attribute describing the agent's identity, system prompt template, and user prompt template. - Register actions in
__init__by callingself._init_actions([MyActionClass, ...]). - (Optional) Override
load_resourceto implement custom resource binding logic. - (Optional) Register dynamic variables using
self._vm.registerto expose runtime data to prompts. - (Optional) Enable function calling by setting
enable_function_call = Trueand populatingavailable_system_tools.
The framework automatically handles resource preloading, prompt generation, LLM invocation via thinking(), parallel action execution via act(), and memory persistence via GptsMemory.
Minimal Example: EchoAgent
Here is a complete, runnable example from packages/derisk-core/src/derisk/agent/expand/echo_agent.py demonstrating the minimal code required to create custom agents:
from derisk.agent.core.base_agent import ConversableAgent
from derisk.agent.core.profile import ProfileConfig, DynConfig
from derisk.agent.expand.actions.agent_action import AgentAction, ActionOutput
class EchoAction(AgentAction):
"""A trivial action that simply echoes the user's message."""
name = "echo"
description = "Return the original user input unchanged."
async def run(self, ai_message: str, **kwargs) -> ActionOutput:
return ActionOutput(content=ai_message, name=self.name, is_exe_success=True)
class EchoAgent(ConversableAgent):
"""Custom agent that repeats whatever the user says."""
profile = ProfileConfig(
name=DynConfig("EchoBot", category="agent", key="custom_echo_name"),
role=DynConfig("Echo Assistant", category="agent", key="custom_echo_role"),
goal=DynConfig(
"Repeat the user's input verbatim",
category="agent",
key="custom_echo_goal",
),
system_prompt_template="You are a simple echo bot. Always repeat the user's last message.",
user_prompt_template="{question}",
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Register the only action this agent can perform
self._init_actions([EchoAction])
async def load_resource(self, question: str, is_retry_chat: bool = False):
"""EchoAgent does not need external resources."""
return None, None
Key implementation details:
- Profile configuration: The
ProfileConfigautomatically injectssystem_prompt_templateanduser_prompt_templateinto the LLM context. - Action registration:
self._init_actions([EchoAction])tells the engine which tools the LLM may invoke. - Resource handling: Overridden
load_resourcereturnsNonebecause this agent requires no external data.
Deploy the agent by constructing it and calling the lifecycle methods:
from derisk.agent.expand.echo_agent import EchoAgent
agent = EchoAgent()
await agent.build() # preload resources, init LLM client
await agent.bind(target=context) # bind an AgentContext
await agent.send(message, recipient=agent) # start conversation
Advanced Example: Knowledge-Enhanced Agent
To create custom agents that interact with external knowledge bases, subclass ConversableAgent and bind a RetrieverResource. This pattern appears in packages/derisk-core/src/derisk/agent/expand/kb_agent.py:
from derisk.agent.core.base_agent import ConversableAgent
from derisk.agent.core.profile import ProfileConfig, DynConfig
from derisk.agent.expand.actions.agent_action import AgentStart, Terminate
from derisk.agent.expand.actions.knowledge_action import KnowledgeSearch
from derisk.agent.resource import RetrieverResource
class KBAgent(ConversableAgent):
"""Agent that answers questions using a bound knowledge base."""
profile = ProfileConfig(
name=DynConfig("KB-Bot", category="agent", key="kb_name"),
role=DynConfig("Knowledge Assistant", category="agent", key="kb_role"),
goal=DynConfig("Find the most relevant knowledge and answer the user.", category="agent", key="kb_goal"),
system_prompt_template=(
"You are a knowledge-retrieval assistant. Use the available knowledge resources."
),
user_prompt_template="{question}",
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Core tools: start, knowledge search, and termination
self._init_actions([AgentStart, KnowledgeSearch, Terminate])
async def preload_resource(self) -> None:
"""Load heavy resources before the first request."""
await super().preload_resource()
await self.system_tool_injection() # inject AgentStart/Terminate etc.
async def load_resource(self, question: str, is_retry_chat: bool = False):
"""If a RetrieverResource is bound, provide its prompt for the LLM."""
if self.resource and isinstance(self.resource, RetrieverResource):
return await self.resource.get_prompt(lang=self.language, question=question)
return None, None
def register_variables(self):
"""Expose knowledge list to the LLM as <knowledge> tags."""
super().register_variables()
@self._vm.register("available_knowledges", "可用知识库")
async def var_knowledges(instance):
prompts = ""
for k, v in self.resource_map.items():
if isinstance(v[0], RetrieverResource):
for item in v:
if hasattr(item, "knowledge_spaces"):
for ks in item.knowledge_spaces:
prompts += (
f"- <knowledge>"
f"<id>{ks.knowledge_id}</id>"
f"<name>{ks.name}</name>"
f"<desc>{ks.desc}</desc>"
f"</knowledge>\n"
)
return prompts
Advanced features demonstrated:
- Resource binding: The agent expects a
RetrieverResource(vector store) attached viaagent.bind(resource=my_retriever). - System tool injection:
system_tool_injection()registers built-in tools likeAgentStartandTerminate. - Dynamic variables:
available_knowledgesrenders as XML tags inside the system prompt, enabling the LLM to see available data sources. - Function calling: Actions like
KnowledgeSearchenable the LLM to trigger vector searches via tool calls.
Architecture: How Custom Agents Execute
When you create custom agents using OpenDerisk, the framework orchestrates the following execution flow automatically:
- Construction: Your subclass initializes with
ProfileConfigand registered actions. - Binding:
agent.bind(target=...)attachesAgentContext, optionalLLMConfig, andResourceobjects. - Preloading:
build()triggerspreload_resource()to load heavy assets and inject system tools. - Prompt generation:
load_resource()generates system/user prompt fragments, whileVariableManagerinjects registered dynamic variables. - LLM invocation:
thinking()sends the composed prompt toself.llm_client. Ifenable_function_callisTrue, the LLM can return tool calls. - Tool parsing:
FunctionCallOutputParserreadstool_callsand instantiatesActionobjects. - Parallel execution:
act()runs all parsed actions concurrently viaasyncio.gather, collectingActionOutputresults. - Verification:
verify()checks response quality and retries up tomax_retry_countif needed. - Memory persistence: All messages and actions are stored via
GptsMemory. - Response streaming: The final
AgentMessagestreams back to the sender viastream_out.
The entire loop is instrumented with tracing spans (root_tracer.start_span) for observability, as implemented in base_agent.py.
Summary
- Subclass
ConversableAgent(single agent) orManagerAgent(team orchestration) to create custom agents with full message handling and memory management. - Configure identity via
ProfileConfigwithsystem_prompt_templateanduser_prompt_templatefor consistent LLM behavior. - Register executable tools by passing
Actionsubclasses toself._init_actions()in the__init__method. - Bind resources (knowledge retrievers, apps) using
agent.bind()and implementload_resource()to format resource data for prompts. - Inject dynamic variables via
self._vm.registerto expose runtime context like available tools or knowledge bases to the LLM. - Lifecycle methods: Always call
await agent.build()andawait agent.bind()before the firstsend()to ensure resources and LLM clients are initialized.
Frequently Asked Questions
What is the difference between ConversableAgent and ManagerAgent when creating custom agents?
ConversableAgent (base_agent.py) provides the fundamental send/receive/reply loop suitable for single-agent conversations. ManagerAgent (base_team.py) extends this with team-management capabilities, allowing you to orchestrate multiple sub-agents, dispatch parallel actions, and coordinate complex multi-step workflows. Choose ConversableAgent for simple chatbots or tool-calling agents, and ManagerAgent when you need hierarchical agent teams.
How do I add custom tools to my OpenDerisk agent?
Create an Action subclass by inheriting from AgentAction in actions/agent_action.py, implement the async def run(self, ...) method returning ActionOutput, and register it in your agent's __init__ via self._init_actions([MyCustomAction]). If enable_function_call is enabled, the framework automatically exposes these actions to the LLM as function-calling tools and handles the FunctionCallOutputParser logic.
Can I bind multiple knowledge bases to a single custom agent?
Yes. The resource_map attribute in ConversableAgent supports multiple Resource objects. In load_resource() or register_variables(), iterate over self.resource_map.items() to access bound resources. Each RetrieverResource can expose its knowledge_spaces via dynamic variables, allowing the LLM to choose which knowledge base to query during the conversation.
What hooks are available for customizing the agent's prompt at runtime?
Override load_resource(self, question, is_retry_chat) to return dynamic system and user prompt fragments based on the current input. Additionally, use register_variables() with self._vm.register to inject dynamic content (like available tools or knowledge lists) that renders into the prompt template before the LLM call. Both methods are called automatically during the prompt generation phase in thinking().
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →