Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@ jobs:
run: |
uv sync --group dev

- name: Run mypy
- name: Run ty
run: |
uv run mypy src/layercode_create_app tests
uv run ty check src/layercode_create_app tests

build:
name: Build Package
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to this project will be documented in this file.

## [0.2.0] - 2025-12-31

### Added

- New `slow_agent`: Testing agent that responds in 3 parts over ~10 seconds, ideal for testing wait/timeout handling in layercode-gym

## [0.1.1] - 2025-12-02

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ lint:
uv run ruff check .

typecheck:
uv run mypy src/layercode_create_app tests
uv run ty check src/layercode_create_app tests

test:
uv run pytest
Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
`layercode-create-app` is a lightweight toolkit for spinning up LayerCode-compatible FastAPI backends with a single command. It packages:

- **Typed LayerCode SDK primitives** for webhook payloads, signature verification, and SSE streaming
- **Ready-to-run agents** (echo, starter, bakery) powered by [PydanticAI](https://ai.pydantic.dev/)
- **Ready-to-run agents** (echo, starter, bakery, outdoor_shop, slow_agent) powered by [PydanticAI](https://ai.pydantic.dev/)
- **Observability hooks** for Logfire + Loguru
- **Built-in Cloudflare tunneling** for instant public webhook URLs
- **An ergonomic CLI** for zero-config development
Expand Down Expand Up @@ -113,7 +113,7 @@ uvx layercode-create-app run --agent bakery --tunnel

| Option | Default | Description |
| --- | --- | --- |
| `--agent` | `starter` | Which built-in agent to run (`echo`, `starter`, `bakery`) |
| `--agent` | `starter` | Which built-in agent to run (`echo`, `starter`, `bakery`, `outdoor_shop`, `slow_agent`) |
| `--model` | env `DEFAULT_MODEL` | Model identifier passed to PydanticAI |
| `--host` | `0.0.0.0` | Server host binding |
| `--port` | `8000` | Server port |
Expand All @@ -132,6 +132,8 @@ uvx layercode-create-app run --agent bakery --tunnel
- **echo** – Deterministic welcome + echo responses (no LLMs required)
- **starter** – Concise general-purpose assistant with progressive disclosure and automatic transcription cleanup
- **bakery** – Bakery persona demonstrating simple tool calls for menu lookup, order placement, and reservations
- **outdoor_shop** – Customer service agent for "Nimbus Gear" outdoor equipment store with complex tool responses and structured data payloads
- **slow_agent** – Testing agent that responds in 3 parts over ~10 seconds; ideal for testing wait/timeout handling in layercode-gym

All prompts live under `src/layercode_create_app/agents/prompts/`. Edit them or add new agents by following the `BaseLayercodeAgent` interface.

Expand Down Expand Up @@ -203,7 +205,9 @@ layercode-create-app/
│ │ ├── base.py
│ │ ├── echo.py
│ │ ├── starter.py
│ │ └── bakery.py
│ │ ├── bakery.py
│ │ ├── outdoor_shop.py
│ │ └── slow_agent.py
│ ├── sdk/ # LayerCode SDK primitives
│ ├── server/ # FastAPI app and routing
│ ├── cli.py # CLI entry point
Expand Down
14 changes: 3 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "layercode-create-app"
version = "0.1.1"
version = "0.2.0"
description = "Fast scaffolding for LayerCode-ready FastAPI backends"
readme = "README.md"
requires-python = ">=3.12"
Expand All @@ -24,7 +24,7 @@ dependencies = [

[project.optional-dependencies]
dev = [
"mypy>=1.11.2",
"ty>=0.0.8",
"pytest>=8.3.2",
"pytest-asyncio>=0.24.0",
"ruff>=0.6.8",
Expand All @@ -34,14 +34,6 @@ dev = [
[project.scripts]
layercode-create-app = "layercode_create_app.cli:main"

[tool.mypy]
python_version = "3.12"
strict = true
warn_unreachable = true
disallow_any_generics = true
disallow_untyped_defs = true
enable_error_code = ["ignore-without-code", "redundant-expr"]

[tool.ruff]
target-version = "py312"
line-length = 100
Expand All @@ -65,7 +57,7 @@ package = true

[dependency-groups]
dev = [
"mypy>=1.18.2",
"ty>=0.0.8",
"pytest>=8.4.2",
"pytest-asyncio>=1.2.0",
"pytest-cov>=6.0.0",
Expand Down
76 changes: 76 additions & 0 deletions src/layercode_create_app/agents/slow_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Slow agent for testing wait/timeout handling in layercode-gym.

This agent responds in 3 parts over ~10 seconds total, with 5-second gaps
between response parts. This stress-tests the 3-second idle timeout in
layercode-gym, making it ideal for validating wait handling and timeout logic.

Response pattern:
1. "Processing..." + data(status=loading, progress=0)
2. [5 second delay]
3. "Still working..." + data(status=processing, progress=50)
4. [5 second delay]
5. "Done!" + data(status=complete, progress=100)

Run with:
uv run layercode-create-app run --agent slow_agent --tunnel --unsafe-update-webhook
"""

from __future__ import annotations

import asyncio

from pydantic_ai.messages import ModelMessage

from ..sdk.events import MessagePayload, SessionEndPayload, SessionStartPayload
from ..sdk.stream import StreamHelper
from .base import BaseLayercodeAgent, agent


@agent("slow_agent")
class SlowAgent(BaseLayercodeAgent):
"""Testing agent that responds in 3 parts over ~10 seconds for wait/timeout testing."""

name = "slow_agent"
description = (
"Testing agent that responds in 3 parts over ~10 seconds (for wait/timeout testing)"
)

def __init__(self, model: str) -> None:
super().__init__(model)

async def handle_session_start(
self, payload: SessionStartPayload, stream: StreamHelper
) -> None:
stream.tts(
"Welcome! I'm a slow agent. "
"Every response takes a bit longer than usual with updates along the way."
)
stream.end()

async def handle_message(
self,
payload: MessagePayload,
stream: StreamHelper,
history: list[ModelMessage],
) -> list[ModelMessage]:
# Part 1: Acknowledge and start loading
stream.tts("Processing your request now. Please wait 5 seconds.")
stream.data({"status": "loading", "progress": 0})

await asyncio.sleep(5)

# Part 2: Progress update
stream.tts(" Still working. Please wait 5 more seconds.")
stream.data({"status": "processing", "progress": 50})

await asyncio.sleep(5)

# Part 3: Complete
stream.tts(" Done! Your request has been processed successfully.")
stream.data({"status": "complete", "progress": 100})
stream.end()

return []

async def handle_session_end(self, payload: SessionEndPayload) -> None:
return None
7 changes: 6 additions & 1 deletion src/layercode_create_app/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .agents import bakery as _bakery # noqa: F401
from .agents import echo as _echo # noqa: F401
from .agents import outdoor_shop as _outdoor_shop # noqa: F401
from .agents import slow_agent as _slow_agent # noqa: F401
from .agents import starter as _starter # noqa: F401
from .config import AppSettings
from .logging import setup_logging
Expand Down Expand Up @@ -159,7 +160,11 @@ def main() -> None:
run_parser.add_argument(
"--agent",
default="starter",
help="Agent to run: echo, starter, bakery, outdoor_shop (default: starter)",
help=(
"Agent to run (default: starter). Options: "
"echo (simple echo), starter (general assistant), bakery (tool demo), "
"outdoor_shop (e-commerce demo), slow_agent (test agent, ~10s response in 3 parts)"
),
)
run_parser.add_argument(
"--model",
Expand Down
10 changes: 5 additions & 5 deletions src/layercode_create_app/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,25 +146,25 @@ async def agent_webhook(request: Request) -> Response:
if payload.type == LayercodeEventType.SESSION_START:
return await _handle_session_start(
payload_dict,
payload,
cast(SessionStartPayload, payload),
agent,
)
if payload.type == LayercodeEventType.MESSAGE:
history = conversation_store.get(payload.conversation_id)
response, new_messages = await _handle_message(
payload_dict,
payload,
cast(MessagePayload, payload),
history,
agent,
)
conversation_store.append(payload.conversation_id, new_messages)
return response
if payload.type == LayercodeEventType.DATA:
return await _handle_data(payload, agent)
return await _handle_data(cast(DataPayload, payload), agent)
if payload.type == LayercodeEventType.SESSION_UPDATE:
return await _handle_session_update(payload, agent)
return await _handle_session_update(cast(SessionUpdatePayload, payload), agent)
if payload.type == LayercodeEventType.SESSION_END:
return await _handle_session_end(payload, agent)
return await _handle_session_end(cast(SessionEndPayload, payload), agent)

raise HTTPException(status.HTTP_400_BAD_REQUEST, "Unsupported event type")
finally:
Expand Down
Loading
Loading