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 asyncioimport jsonimport timefrom typing import Dict, Any, Optional
from pymongo import AsyncMongoClient
from flexus_client_kit import ckit_clientfrom flexus_client_kit import ckit_cloudtoolfrom flexus_client_kit import ckit_bot_execfrom flexus_client_kit import ckit_shutdownfrom flexus_client_kit import ckit_ask_modelfrom flexus_client_kit import ckit_mongofrom flexus_client_kit import ckit_kanbanfrom flexus_client_kit import ckit_external_authfrom flexus_client_kit import erp_schemafrom flexus_client_kit.integrations import fi_mongo_storefrom flexus_client_kit.integrations import fi_pdocfrom 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
### ribbitUse this to greet users with frog sounds.
### catch_insectsCatch insects in parallel. Requires tongue capacity from setup.
### make_pond_reportCreate a pond report document with name, weather, and mood.
### mongo_storeStore personal data persistently.
### flexus_policy_documentRead and write policy documents.
""" + prompts_common.PROMPT_POLICY_DOCUMENTS + prompts_common.PROMPT_KANBAN
# Huntmode expert - for subchats that return immediatelySYSTEM_PROMPT_HUNTMODE = """You are in hunting mode. Catch the insect!"""
# Kernel for huntmode - returns immediatelyKERNEL_HUNTMODE = """subchat_result = "Insect caught!""""frog_install.py
import jsonfrom flexus_client_kit import ckit_bot_installfrom flexus_client_kit.ckit_bot_install import FMarketplaceExpertInputfrom flexus_simple_bots import prompts_commonfrom 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
- TOOLS list — Define all tools your bot can use
- Every tool needs a handler —
@rcx.on_tool_call(TOOL.name) - Setup mixing — Merge defaults with user config
- Integrations — Use built-in fi_pdoc and fi_mongo_store
- ERP subscriptions — React to database changes
- Subchats — Delegate work to specialized experts
- 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).