Loading the notebook…
Loading the notebook…
Notebooks · Output, pipelines and memory · 11
Chain agents and functions together into deterministic workflows. Cleaner than LangChain's LCEL.
Its imports and arguments are checked against shipit-agent 3.0. The outputs were saved when it was written, for an earlier release.
from pathlib import Path
import sys
ROOT = (
Path.cwd().resolve().parent
if Path.cwd().name == "notebooks"
else Path.cwd().resolve()
)
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from shipit_agent import Agent, Pipeline, step, parallel
from examples.run_multi_tool_agent import build_llm_from_env
from IPython.display import display, Markdown
llm = build_llm_from_env("bedrock")# Create specialized agents
researcher = Agent(
llm=llm, prompt="You are a research expert. Provide key facts only, be concise."
)
writer = Agent(
llm=llm, prompt="You are a technical writer. Write clear, engaging content."
)
# Chain them: research first, then write using the research
pipe = Pipeline.sequential(
step("research", agent=researcher, prompt="Find 3 key facts about {topic}"),
step(
"write",
agent=writer,
prompt="Write a short paragraph using these facts:\n{research.output}",
),
)
result = pipe.run(topic="Python asyncio library")
print("=== Research Output ===")
print(result.steps["research"].output[:300])
print("\n=== Final Written Output ===")
display(Markdown(result.output))=== Research Output === - **Concurrency model:** `asyncio` provides a single‑threaded, event‑loop based framework where `async def` coroutines, `await` expressions, and non‑blocking I/O enable thousands of concurrent tasks without OS threads. - **Core primitives:** The library ships with high‑level constructs such as `asy === Final Written Output ===
asyncio offers a single‑threaded, event‑loop concurrency model: async def coroutines, await expressions, and non‑blocking I/O let you run thousands of tasks side‑by‑side without ever spawning OS threads. The library ships with high‑level primitives such as asyncio.run(), asyncio.create_task(), asyncio.gather(), asyncio.wait(), and synchronization tools like Queue, Lock, and Event, while the low‑level BaseTransport/Protocol APIs enable developers to craft their own async protocols. Since its inclusion in the Python standard library in 3.4, asyncio has become the lingua‑franca for asynchronous Python, and it plays nicely with other async ecosystems through adapters like asyncio.run_in_executor, loop.add_reader/writer, and asyncio.to_thread. Third‑party packages that expose an awaitable interface—such as aiohttp, aiopg, or httpx—can be dropped in directly, giving you a seamless, portable way to build scalable network‑oriented applications.
display(Markdown(result.output))asyncio offers a single‑threaded, event‑loop concurrency model: async def coroutines, await expressions, and non‑blocking I/O let you run thousands of tasks side‑by‑side without ever spawning OS threads. The library ships with high‑level primitives such as asyncio.run(), asyncio.create_task(), asyncio.gather(), asyncio.wait(), and synchronization tools like Queue, Lock, and Event, while the low‑level BaseTransport/Protocol APIs enable developers to craft their own async protocols. Since its inclusion in the Python standard library in 3.4, asyncio has become the lingua‑franca for asynchronous Python, and it plays nicely with other async ecosystems through adapters like asyncio.run_in_executor, loop.add_reader/writer, and asyncio.to_thread. Third‑party packages that expose an awaitable interface—such as aiohttp, aiopg, or httpx—can be dropped in directly, giving you a seamless, portable way to build scalable network‑oriented applications.
# Mix LLM agents with pure Python functions — no LLM call needed for deterministic transforms
def word_count(text: str) -> str:
words = len(text.split())
return f"Word count: {words}. Original text length: {len(text)} chars."
def uppercase(text: str) -> str:
return text.upper()
pipe = Pipeline.sequential(
step(
"generate",
agent=Agent(llm=llm, prompt="You are creative."),
prompt="Write a haiku about {topic}",
),
step("stats", fn=word_count), # pure Python — no LLM call
step("shout", fn=uppercase), # another pure function
)
result = pipe.run(topic="coding")
print("Generated:", result.steps["generate"].output)
print("Stats:", result.steps["stats"].output)
print("Shouted:", result.output)Generated: Silent keys whisper Loops unwind like sunrise code Bugs fade with sunrise Stats: Word count: 12. Original text length: 77 chars. Shouted: WORD COUNT: 12. ORIGINAL TEXT LENGTH: 77 CHARS.
import time
# Run 3 agents in parallel, then merge results with a 4th agent
pros_agent = Agent(llm=llm, prompt="List only the pros/advantages. Be concise.")
cons_agent = Agent(llm=llm, prompt="List only the cons/disadvantages. Be concise.")
synthesizer = Agent(
llm=llm, prompt="You are a balanced analyst. Synthesize both sides."
)
pipe = Pipeline(
# These 2 steps run concurrently (parallel fan-out)
parallel(
step("pros", agent=pros_agent, prompt="Pros of {topic}"),
step("cons", agent=cons_agent, prompt="Cons of {topic}"),
),
# This step runs after both parallel steps complete
step(
"synthesis",
agent=synthesizer,
prompt="Combine these viewpoints:\n\nPros:\n{pros.output}\n\nCons:\n{cons.output}",
),
)
start = time.time()
result = pipe.run(topic="microservices architecture")
elapsed = time.time() - start
print(f"Completed in {elapsed:.1f}s\n")
print("=== Pros ===")
print(result.steps["pros"].output)
print("\n=== Cons ===")
print(result.steps["cons"].output)
print("\n=== Synthesis ===")
display(Markdown(result.output))Completed in 19.7s === Pros === - **Independent deployment** – each service can be released, scaled, or rolled back without impacting the whole system. - **Technology heterogeneity** – teams can choose the best language, framework, or database per service. - **Scalability** – resources can be allocated to high‑load services only, improving cost‑efficiency. - **Fault isolation** – failures stay confined to the offending service, preserving overall system availability. - **Faster development cycles** – smaller codebases enable quicker iteration and easier onboarding of new developers. - **Team autonomy** – bounded‑context services align with cross‑functional teams, reducing coordination overhead. - **Improved maintainability** – clear service boundaries simplify code comprehension, testing, and refactoring. - **Continuous delivery / DevOps friendliness** – pipelines can be built per service, enabling rapid CI/CD. - **Better alignment with business domains** – services map directly to business capabilities, facilitating clearer ownership and evolution. - **Facilitates reuse** – services can be consumed by multiple applications or channels (mobile, web, IoT). === Cons === - Increased operational complexity and overhead - Higher latency and network reliability concerns - Distributed data management and consistency challenges - Difficulties in end‑to‑end testing and debugging - More elaborate deployment pipelines and CI/CD requirements - Greater need for robust monitoring, logging, and tracing - Larger surface area for security vulnerabilities - Cross‑service transaction and orchestration complications - Higher infrastructure and resource costs - Necessity for specialized DevOps and architectural expertise. === Synthesis ===
Below is an integrated view that weaves together the advantages and the drawbacks you listed, adds a few contextual qualifiers, and offers practical ways to harness the benefits while temper‑ing the risks.
| Core Benefit | What it Enables | Typical Business Value |
|---|---|---|
| Independent deployment | Release, scale or roll‑back a single service without touching the rest of the code base. | Faster time‑to‑market for new features or bug fixes; reduced risk of “big‑bang” releases. |
| Technology heterogeneity | Teams can pick the language, framework, or DB that best fits a given problem domain. | Improves productivity and performance (e.g., using a low‑latency C++ service for pricing, a Python service for ML inference). |
| Scalability on demand | Allocate CPU/Memory only to the hot‑spot services. | Lower cloud spend and better response times under load spikes. |
| Fault isolation | Failure stays within the offending boundary. | Higher overall system availability; graceful degradation instead of full‑system outage. |
| Faster development cycles | Smaller codebases, clearer ownership, easier onboarding. | Shorter sprint cycles, higher morale, quicker learning curves for new hires. |
| Team autonomy (bounded‑contexts) | Cross‑functional squads own a service end‑to‑end. | Reduces coordination overhead, aligns engineering with business domains. |
| Improved maintainability | Clear contract (API) and bounded responsibilities. | Simpler refactoring, easier reasoning about change impact, lower technical debt. |
| CI/CD friendliness | Pipelines can be scoped to a single repository / service. | Enables true continuous delivery, automated testing per service, rapid rollback. |
| Business‑domain alignment | Services map 1‑1 (or close) to capabilities like “Payments”, “Catalog”. | Clear ownership, facilitates product‑driven roadmaps, enables reuse across channels. |
| Facilitates reuse | Same service can serve web, mobile, partner APIs, IoT, etc. | Reduces duplication, accelerates multi‑channel expansion. |
Bottom line: When a system is large, grows quickly, or must serve many distinct business capabilities, the above benefits can translate into measurable competitive advantage.
| Challenge | Technical Implication | Business Impact if Ignored |
|---|---|---|
| Operational complexity & overhead | Multiple services → many hosts, containers, networking configs, service discovery, etc. | Increased ops head‑count, higher chance of misconfiguration. |
| Higher latency & network reliability | Every remote call adds round‑trip time and introduces potential failure points. | Slower end‑user experience, possible SLA breach. |
| Distributed data & consistency | Each service may own its own datastore; ACID transactions across services become hard. | Data anomalies, eventual‑consistency pitfalls, harder debugging of business bugs. |
| End‑to‑end testing & debugging | Integration tests must spin up many services; tracing failures across process boundaries is non‑trivial. | Longer QA cycles, higher defect escape rate. |
| Elaborate CI/CD pipelines | Need per‑service build, test, promotion, versioning, and release coordination. | Pipeline sprawl, risk of version drift, slower releases if pipelines are brittle. |
| Observability demands (monitoring, logging, tracing) | Centralized metrics, structured logs, distributed tracing become mandatory. | Blind spots → delayed incident response, harder root‑cause analysis. |
| Expanded security surface | More endpoints, inter‑service auth, secret management, network policies. | Greater attack vector; potential data breach if security is lax. |
| Cross‑service transaction & orchestration | Compensatio | |
| … output trimmed |
# Conditional routing: classify first, then route to the right handler
classifier = Agent(
llm=llm,
prompt="Classify the input as 'question' or 'statement'. Reply with just the word.",
)
question_agent = Agent(llm=llm, prompt="You answer questions helpfully and concisely.")
statement_agent = Agent(
llm=llm, prompt="You acknowledge statements and add interesting context."
)
pipe = Pipeline.sequential(
step("classify", agent=classifier, prompt="{input}"),
step(
"handle",
router=lambda ctx: "question"
if "question" in ctx["classify"].output.lower()
else "statement",
branches={
"question": step("answer", agent=question_agent, prompt="{input}"),
"statement": step("respond", agent=statement_agent, prompt="{input}"),
},
),
)
# Test with a question
result = pipe.run(input="What is the speed of light?")
print("Classification:", result.steps["classify"].output.strip())
print("Route taken:", "question" if "answer" in result.steps else "statement")
display(Markdown(result.output))Classification: question Route taken: question Response: The speed of light in a vacuum is a defined constant: **c = 299 792 458 m · s⁻¹** That is: - **≈ 186 282 miles per second** - **≈ 1.08 × 10⁹ km · h⁻¹** - **≈ 3.00 × 10⁸ m · s⁻¹** (often rounded for calculations) This value is exact because the metre is defined in terms of the distance light travels in a specified fraction of a second.