Your First Amphibious Agent¶
Documentation update in progress
This Amphibious tutorial is currently outdated and may not match the bridgic-amphibious 0.2.0 API. We are actively updating it. Please use it as conceptual background only until the update is complete.
Build your first agent in 5 minutes. You'll create the same task twice — once in Agent mode (the LLM decides what to do) and once in Workflow mode (you decide what to do) — and see how a single framework supports both paradigms.
Practical Scenario¶
We'll build a small "weather information assistant" that looks up weather for cities.
- In Agent mode, the LLM autonomously decides which cities to check and how to summarize the results.
- In Workflow mode, you spell out the exact cities and the order of the steps.
Initialize¶
First, set up the LLM. The examples read credentials from environment variables, so set MODEL_NAME, API_KEY, and BASE_URL before running.
import os
model_name = os.environ.get("MODEL_NAME")
api_key = os.environ.get("API_KEY")
api_base = os.environ.get("BASE_URL")
from bridgic.llms.openai import OpenAILlm, OpenAIConfiguration
llm = OpenAILlm(
api_key=api_key,
api_base=api_base,
timeout=30,
configuration=OpenAIConfiguration(
model=model_name,
temperature=0.0,
max_tokens=16384,
),
)
Define a Tool¶
A tool is just an async function with a docstring. Here is a mock weather lookup the agent can call.
async def get_weather(city: str) -> str:
"""Get the current weather for a city."""
return f"Sunny, 22 C in {city}"
The Two-Loop Context Model¶
Every amphibious agent is parameterized by two contexts — AmphibiousAutoma[OTAContext, Context]:
OTAContext— the small loop, owned by the framework. It holds this run'suser_input, the observe-think-act round trace (ota_record), and thetoolsthe run carries. The framework builds a fresh one per run.Context— the big loop, free-form cross-turn knowledge. Define fields and (optionally) overridesummary(); your worker'sthinking()folds it into the prompt when you need it (nothing is auto-injected).
Tools are declared on the OTA context — nothing is auto-injected. Use OTAContext.tool(...) as a decorator or a call. Whatever you declare is exactly what the run's tools field holds.
from bridgic.amphibious import OTAContext, Context
# Small-loop context: declare the tools this run carries.
class WeatherOTAContext(OTAContext):
pass
WeatherOTAContext.tool(get_weather)
# Big-loop context: free-form knowledge (bare here — nothing extra to render).
class WeatherContext(Context):
pass
Agent Mode — the LLM decides¶
In Agent mode you declare a think unit (a CognitiveWorker) and orchestrate it in on_agent. The worker owns one method, thinking(): it assembles a prompt from the contexts, calls the model, and returns the call's natural result — the framework adapts that into a decision and runs any tool calls. on_agent yields ThinkUnit("name") to drive the cognitive loop (capped by max_attempts, ended when the LLM stops calling tools).
from bridgic.amphibious import AmphibiousAutoma, CognitiveWorker, think_unit, ThinkUnit
from bridgic.core.model.types import Message, Role
class WeatherThink(CognitiveWorker):
async def thinking(self, ota_context, context=None):
messages = [Message.from_text(ota_context.summary(), role=Role.USER)]
# aselect_tool returns (tool_calls, content); the framework turns that
# into a decision. A reply with no tool calls is the finish.
return await self._llm.aselect_tool(
messages=messages,
tools=[t.to_tool() for t in ota_context.tools],
)
class WeatherAgent(AmphibiousAutoma[WeatherOTAContext, WeatherContext]):
planner = think_unit(WeatherThink(), max_attempts=5)
async def on_agent(self, ota_context, context=None):
yield ThinkUnit("planner")
# Only on_agent is overridden, so AUTO resolves to AGENT mode.
agent = WeatherAgent(verbose=True)
answer = await agent.arun(
llm=llm,
user_input="Check the weather in Tokyo and London, then summarize.",
)
print(answer) # the finishing think step's step_content
print(agent.final_answer)
Workflow Mode — you decide¶
In Workflow mode you write the exact steps in on_workflow, yielding ActionCall to run a tool. result = yield ActionCall(...) hands you back a List[ToolResult]. A pure workflow needs no LLM — only on_workflow is overridden, so AUTO resolves to WORKFLOW.
from bridgic.amphibious import ActionCall, RETURN
class WeatherWorkflow(AmphibiousAutoma[WeatherOTAContext, WeatherContext]):
async def on_workflow(self, ota_context, context=None):
tokyo = yield ActionCall("get_weather", city="Tokyo") # List[ToolResult]
london = yield ActionCall("get_weather", city="London")
tokyo_val = tokyo[0].result if tokyo else "N/A"
london_val = london[0].result if london else "N/A"
yield RETURN(f"Tokyo: {tokyo_val} | London: {london_val}")
workflow = WeatherWorkflow(verbose=True)
result = await workflow.arun(user_input="Check weather") # no llm needed
print(result)
Agent vs Workflow¶
Agent mode (on_agent) | Workflow mode (on_workflow) | |
|---|---|---|
| Who decides the steps | the LLM | you |
| Building block | ThinkUnit (drives a CognitiveWorker) | ActionCall / HumanCall / LLMCall |
| Needs an LLM | yes | no (pure workflow) |
| Best for | open-ended, adaptive tasks | known, repeatable processes |
You don't have to choose one forever: override both and the framework runs Amphiflow — a deterministic workflow that falls back to a bounded agent sub-run when a step fails. That's covered in the RunMode tutorial.
Next Steps¶
- Dual-Mode Orchestration —
on_agentvson_workflow, and switching withEnterAgent. - CognitiveWorker & think_unit — the atomic think unit in depth.
- Built-in Tools — declare the shipped tools on your OTA context.