Skip to content

Pattern: Basic Bot

The Basic Bot pattern is the foundation for most Flexus bots. It combines:

  • Custom tools
  • Policy document storage
  • Personal MongoDB
  • Optional ERP subscriptions

Reference implementation: flexus_simple_bots/frog/ — the Frog bot

When to Use

Use this pattern when you need a bot that:

  • Responds to user conversations
  • Stores persistent data
  • Reads/writes documents
  • Optionally reacts to ERP changes

Architecture

┌─────────────────────────────────────────┐
│ FROG BOT │
├─────────────────────────────────────────┤
│ Tools: │
│ - ribbit (custom greeting) │
│ - catch_insects (spawns subchats) │
│ - make_pond_report (creates pdoc) │
│ - mongo_store (personal storage) │
│ - policy_document (read/write docs) │
├─────────────────────────────────────────┤
│ Event Handlers: │
│ - @on_updated_message │
│ - @on_updated_task │
│ - @on_erp_change("crm_contact") │
└─────────────────────────────────────────┘

Complete Implementation

frog_bot.py

import asyncio
import json
import time
from typing import Dict, Any, Optional
from pymongo import AsyncMongoClient
from flexus_client_kit import ckit_client
from flexus_client_kit import ckit_cloudtool
from flexus_client_kit import ckit_bot_exec
from flexus_client_kit import ckit_shutdown
from flexus_client_kit import ckit_ask_model
from flexus_client_kit import ckit_mongo
from flexus_client_kit import ckit_kanban
from flexus_client_kit import ckit_external_auth
from flexus_client_kit import erp_schema
from flexus_client_kit.integrations import fi_mongo_store
from flexus_client_kit.integrations import fi_pdoc
from frog import frog_install
BOT_NAME = "frog"
BOT_VERSION = "1.0.0"
# --- Tool Definitions ---
RIBBIT_TOOL = ckit_cloudtool.CloudTool(
strict=True,
name="ribbit",
description="Make a frog sound to greet users or express happiness.",
parameters={
"type": "object",
"properties": {
"intensity": {
"type": "string",
"enum": ["quiet", "normal", "loud"],
"description": "How loud the ribbit should be",
},
"message": {
"type": ["string", "null"],
"description": "Optional message to include",
},
},
"required": ["intensity", "message"],
"additionalProperties": False,
},
)
CATCH_INSECTS_TOOL = ckit_cloudtool.CloudTool(
strict=True,
name="catch_insects",
description="Catch insects in parallel using subchats.",
parameters={
"type": "object",
"properties": {
"N": {
"type": "integer",
"description": "Number of parallel catch attempts",
},
},
"required": ["N"],
"additionalProperties": False,
},
)
MAKE_POND_REPORT_TOOL = ckit_cloudtool.CloudTool(
strict=True,
name="make_pond_report",
description="Create a pond report document.",
parameters={
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path for the report (e.g., '/frog/monday-report')",
},
"report": {
"type": "object",
"properties": {
"pond_name": {"type": "string"},
"weather": {"type": "string", "enum": ["sunny", "cloudy", "rainy", "stormy"]},
"mood": {"type": "string", "enum": ["happy", "excited", "calm", "hungry"]},
},
"required": ["pond_name", "weather", "mood"],
"additionalProperties": False,
},
},
"required": ["path", "report"],
"additionalProperties": False,
},
)
TOOLS = [
RIBBIT_TOOL,
CATCH_INSECTS_TOOL,
MAKE_POND_REPORT_TOOL,
fi_mongo_store.MONGO_STORE_TOOL,
fi_pdoc.POLICY_DOCUMENT_TOOL,
]
# --- Main Loop ---
async def frog_main_loop(fclient: ckit_client.FlexusClient, rcx: ckit_bot_exec.RobotContext):
# Merge default setup with user overrides
setup = ckit_bot_exec.official_setup_mixing_procedure(
frog_install.frog_setup_schema,
rcx.persona.persona_setup
)
# Initialize MongoDB
mongo_conn_str = await ckit_mongo.mongo_fetch_creds(fclient, rcx.persona.persona_id)
mongo = AsyncMongoClient(mongo_conn_str)
db = mongo[rcx.persona.persona_id + "_db"]
personal_mongo = db["personal_mongo"]
# Initialize Policy Documents integration
pdoc_integration = fi_pdoc.IntegrationPdoc(rcx, rcx.persona.ws_root_group_id)
# Track tongue capacity for catch_insects
tongue_capacity_used = {}
# --- Event Handlers ---
@rcx.on_updated_message
async def updated_message_in_db(msg):
pass # Log or react to messages if needed
@rcx.on_updated_thread
async def updated_thread_in_db(thread):
pass # Log or react to thread changes
@rcx.on_updated_task
async def updated_task_in_db(task):
print(f"Task update: {task.ktask_title} -> {task.ktask_column}")
@rcx.on_erp_change("crm_contact")
async def on_contact_change(action: str, new_record: Optional[erp_schema.CrmContact], old_record):
# React to CRM changes
if action == "INSERT":
print(f"New contact: {new_record.contact_first_name}")
elif action == "UPDATE":
print(f"Contact updated: {new_record.contact_first_name}")
# --- Tool Handlers ---
@rcx.on_tool_call(RIBBIT_TOOL.name)
async def toolcall_ribbit(toolcall, args: Dict[str, Any]) -> str:
intensity = args.get("intensity", "normal")
message = args.get("message", "")
sounds = {
"quiet": "ribbit...",
"normal": "RIBBIT!",
"loud": "RIBBIT RIBBIT RIBBIT!!!",
}
result = sounds.get(intensity, "RIBBIT!")
if message:
result += f" {message}"
return result
@rcx.on_tool_call(CATCH_INSECTS_TOOL.name)
async def toolcall_catch_insects(toolcall, args: Dict[str, Any]) -> str:
N = args.get("N", 1)
capacity = setup.get("tongue_capacity", 5)
pid = rcx.persona.persona_id
used = tongue_capacity_used.get(pid, 0)
remaining = capacity - used
if N < 1:
return "Error: N must be positive."
if N > remaining:
return f"Error: Only {remaining}/{capacity} capacity left."
tongue_capacity_used[pid] = used + N
# Spawn parallel subchats
subchats = await ckit_ask_model.bot_subchat_create_multiple(
client=fclient,
who_is_asking="frog_catch_insects",
persona_id=rcx.persona.persona_id,
first_question=[f"Catch insect #{i+1}!" for i in range(N)],
first_calls=["null" for _ in range(N)],
title=[f"Catching #{i+1}" for i in range(N)],
fcall_id=toolcall.fcall_id,
fexp_name="huntmode", # Uses huntmode expert
)
raise ckit_cloudtool.WaitForSubchats(subchats)
@rcx.on_tool_call(MAKE_POND_REPORT_TOOL.name)
async def toolcall_make_pond_report(toolcall, args: Dict[str, Any]) -> str:
path = args["path"]
report = args["report"]
# Create document with meta structure for custom forms
doc = {
"pond_report": {
"meta": {"created_at": time.strftime("%Y-%m-%d %H:%M:%S")},
**report,
}
}
fuser_id = ckit_external_auth.get_fuser_id_from_rcx(rcx, toolcall.fcall_ft_id)
await pdoc_integration.pdoc_create(path, json.dumps(doc), fuser_id)
return f"Report created at {path}"
@rcx.on_tool_call(fi_mongo_store.MONGO_STORE_TOOL.name)
async def toolcall_mongo_store(toolcall, args):
return await fi_mongo_store.handle_mongo_store(rcx.workdir, personal_mongo, toolcall, args)
@rcx.on_tool_call(fi_pdoc.POLICY_DOCUMENT_TOOL.name)
async def toolcall_pdoc(toolcall, args):
return await pdoc_integration.called_by_model(toolcall, args)
# --- Main Loop ---
try:
while not ckit_shutdown.shutdown_event.is_set():
await rcx.unpark_collected_events(sleep_if_no_work=10.0)
finally:
mongo.close()
def main():
scenario_fn = ckit_bot_exec.parse_bot_args()
fclient = ckit_client.FlexusClient(
ckit_client.bot_service_name(BOT_NAME, BOT_VERSION),
endpoint="/v1/jailed-bot"
)
asyncio.run(ckit_bot_exec.run_bots_in_this_group(
fclient,
marketable_name=BOT_NAME,
marketable_version_str=BOT_VERSION,
bot_main_loop=frog_main_loop,
inprocess_tools=TOOLS,
scenario_fn=scenario_fn,
install_func=frog_install.install,
subscribe_to_erp_tables=["crm_contact"], # ERP subscription
))
if __name__ == "__main__":
main()

frog_prompts.py

from flexus_simple_bots import prompts_common
SYSTEM_PROMPT_DEFAULT = """You are Frog, a friendly amphibian assistant.
## Your Personality
- Cheerful and helpful
- Love catching insects
- Enjoy creating pond reports
## Tools
### ribbit
Use this to greet users with frog sounds.
### catch_insects
Catch insects in parallel. Requires tongue capacity from setup.
### make_pond_report
Create a pond report document with name, weather, and mood.
### mongo_store
Store personal data persistently.
### flexus_policy_document
Read and write policy documents.
""" + prompts_common.PROMPT_POLICY_DOCUMENTS + prompts_common.PROMPT_KANBAN
# Huntmode expert - for subchats that return immediately
SYSTEM_PROMPT_HUNTMODE = """You are in hunting mode. Catch the insect!"""
# Kernel for huntmode - returns immediately
KERNEL_HUNTMODE = """
subchat_result = "Insect caught!"
"""

frog_install.py

import json
from flexus_client_kit import ckit_bot_install
from flexus_client_kit.ckit_bot_install import FMarketplaceExpertInput
from flexus_simple_bots import prompts_common
from frog import frog_bot, frog_prompts
frog_setup_schema = {
"tongue_capacity": {
"bs_type": "int",
"bs_default": 5,
"bs_group": "behavior",
"bs_description": "Maximum parallel insect catches",
},
}
tools_json = json.dumps([t.openai_style_tool() for t in frog_bot.TOOLS])
def install(ws_id: str):
return ckit_bot_install.marketplace_upsert_dev_bot(
ws_id=ws_id,
marketable_name="frog",
marketable_version="1.0.0",
marketable_title1="Frog",
marketable_title2="A friendly frog that demonstrates Flexus patterns",
marketable_occupation="Demo Assistant",
marketable_author="Flexus Team",
marketable_description="Educational bot demonstrating basic patterns.",
marketable_typical_group="Demo / Learning",
marketable_tags=["demo", "tutorial"],
marketable_setup_schema=json.dumps(frog_setup_schema),
marketable_picture_big_b64=ckit_bot_install.load_image_b64(__file__, "frog-1024x1536.webp"),
marketable_picture_small_b64=ckit_bot_install.load_image_b64(__file__, "frog-256x256.webp"),
marketable_experts=[
("default", FMarketplaceExpertInput(
fexp_system_prompt=frog_prompts.SYSTEM_PROMPT_DEFAULT,
fexp_app_capture_tools=tools_json,
)),
("huntmode", FMarketplaceExpertInput(
fexp_system_prompt=frog_prompts.SYSTEM_PROMPT_HUNTMODE,
fexp_python_kernel=frog_prompts.KERNEL_HUNTMODE,
fexp_app_capture_tools="[]", # No tools needed
)),
],
marketable_schedule=[
prompts_common.SCHED_TASK_SORT_10M,
prompts_common.SCHED_TODO_5M,
],
marketable_forms=ckit_bot_install.load_form_bundles(__file__),
)
if __name__ == "__main__":
ckit_bot_install.main_install_dev_bot(install)

Key Takeaways

  1. TOOLS list — Define all tools your bot can use
  2. Every tool needs a handler@rcx.on_tool_call(TOOL.name)
  3. Setup mixing — Merge defaults with user config
  4. Integrations — Use built-in fi_pdoc and fi_mongo_store
  5. ERP subscriptions — React to database changes
  6. Subchats — Delegate work to specialized experts
  7. Graceful shutdown — Use ckit_shutdown.shutdown_event

Variations

Without ERP Subscription

Remove subscribe_to_erp_tables parameter and @on_erp_change handler.

Without Subchats

Remove tools that spawn subchats and the corresponding expert.

With Messenger Integration

Add Slack/Discord integration (see Messenger Bot Pattern).