-
Notifications
You must be signed in to change notification settings - Fork 26
馃Ч chore: Py example cassette #331
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| # Python-generated files | ||
| __pycache__/ | ||
| *.py[oc] | ||
| build/ | ||
| dist/ | ||
| wheels/ | ||
| *.egg-info | ||
| .pytest_cache/ | ||
|
|
||
| # Virtual environments | ||
| .venv | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| 3.14 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| FROM ghcr.io/astral-sh/uv:python3.14-bookworm-slim AS build | ||
|
|
||
| WORKDIR /app | ||
| ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_PYTHON_DOWNLOADS=never | ||
| COPY pyproject.toml uv.lock README.md ./ | ||
| RUN uv sync --frozen --no-dev --no-install-project | ||
| COPY main.py ./ | ||
|
|
||
| FROM python:3.14-slim-bookworm | ||
|
|
||
| WORKDIR /app | ||
| COPY --from=build /app/.venv /app/.venv | ||
| COPY --from=build /app/main.py ./ | ||
| ENV PATH="/app/.venv/bin:$PATH" CASSETTE_LISTEN=0.0.0.0:9999 | ||
| EXPOSE 9999 | ||
| USER 65532:65532 | ||
| ENTRYPOINT ["python", "main.py"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # Prompt cassette | ||
|
|
||
| A minimal [Tapes](https://github.com/papercomputeco/tapes) cassette that returns | ||
| a session's first captured user prompt and advertises it as an MCP tool. | ||
|
|
||
| ## Run | ||
|
|
||
| ```sh | ||
| docker compose up --build | ||
| curl http://127.0.0.1:9999/openapi | ||
| ``` | ||
|
|
||
| Compose starts PostgreSQL, Tapes (read API on `8081`, ingest on `8082`), and the | ||
| cassette on `9999`. Tapes loads the cassette from `http://prompt:9999/openapi`. | ||
|
|
||
| The operation-level `x-tapes-mcp` declaration publishes | ||
| `prompt.get_prompt` through Tapes' streamable HTTP MCP endpoint at | ||
| `http://127.0.0.1:8081/v1/mcp`. | ||
|
|
||
| The cassette also works directly over HTTP: | ||
|
|
||
| ```sh | ||
| curl -X POST http://127.0.0.1:9999/api/prompt/get \ | ||
| -H 'Content-Type: application/json' \ | ||
| -d '{"session_id":"00000000-0000-0000-0000-000000000001"}' | ||
| ``` | ||
|
|
||
| ## Local Python with `uv` | ||
|
|
||
| ```sh | ||
| uv venv | ||
| source .venv/bin/activate | ||
| uv sync | ||
| python main.py | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| kind = "cassette/v1alpha1" | ||
|
|
||
| [cassette] | ||
| name = "prompt" | ||
| version = "0.1.0" | ||
| display_name = "Prompt" | ||
| description = "Returns the first captured user prompt for a tapes session." | ||
| license = "Apache-2.0" | ||
| homepage = "https://github.com/papercomputeco/prompt-cassette" | ||
| image = "tapes/prompt-cassette:0.1.0" | ||
| port = 9999 | ||
|
|
||
| [depends] | ||
| core = "v1" | ||
| views = [] | ||
|
|
||
| [api] | ||
| health = "/ping" | ||
| openapi = "/openapi" | ||
| prefix_path = "api" | ||
|
|
||
| [[config]] | ||
| key = "tapes_base_url" | ||
| type = "string" | ||
| default = "http://127.0.0.1:8081" | ||
| description = "Base URL of the tapes core API." |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| name: tapes-prompt-cassette | ||
|
|
||
| services: | ||
| postgres: | ||
| image: public.ecr.aws/g4e5l3z3/papercomputeco/postgres:17.7-pgduckdb-1.1.1 | ||
| environment: | ||
| POSTGRES_DB: tapes | ||
| POSTGRES_USER: tapes | ||
| POSTGRES_PASSWORD: tapes | ||
| volumes: | ||
| - postgres-data:/var/lib/postgresql/data | ||
| healthcheck: | ||
| test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U tapes -d tapes"] | ||
| interval: 1s | ||
| timeout: 3s | ||
| retries: 30 | ||
|
|
||
| prompt: | ||
| build: . | ||
| environment: | ||
| CASSETTE_TAPES_BASE_URL: http://tapes:8081 | ||
| ports: | ||
| - "127.0.0.1:9999:9999" | ||
|
|
||
| tapes: | ||
| image: public.ecr.aws/g4e5l3z3/papercomputeco/tapes:latest | ||
| command: >- | ||
| serve | ||
| --api-listen 0.0.0.0:8081 | ||
| --ingest-listen 0.0.0.0:8082 | ||
| --postgres postgres://tapes:tapes@postgres:5432/tapes?sslmode=disable | ||
| --cassettes http://prompt:9999/openapi | ||
| --cassette-refresh 10s | ||
| --embed-spans=false | ||
| ports: | ||
| - "127.0.0.1:8081:8081" | ||
| - "127.0.0.1:8082:8082" | ||
| depends_on: | ||
| postgres: | ||
| condition: service_healthy | ||
|
|
||
| volumes: | ||
| postgres-data: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,201 @@ | ||
| """A minimal tapes cassette that returns a session's first user prompt.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import os | ||
| from typing import Any | ||
| from uuid import UUID | ||
|
|
||
| import httpx | ||
| from fastapi import FastAPI, HTTPException | ||
| from fastapi.responses import JSONResponse | ||
| from pydantic import BaseModel | ||
|
|
||
| MANIFEST: dict[str, Any] = { | ||
| "kind": "cassette/v1alpha1", | ||
| "cassette": { | ||
| "name": "prompt", | ||
| "version": "0.1.0", | ||
| "display_name": "Prompt", | ||
| "description": "Returns the first captured user prompt for a tapes session.", | ||
| "license": "Apache-2.0", | ||
| "homepage": "https://github.com/papercomputeco/prompt-cassette", | ||
| "image": "tapes/prompt-cassette:0.1.0", | ||
| "port": 9999, | ||
| }, | ||
| "depends": {"core": "v1", "views": []}, | ||
| "api": {"health": "/ping", "openapi": "/openapi", "prefix_path": "api"}, | ||
| "config": [ | ||
| { | ||
| "key": "tapes_base_url", | ||
| "type": "string", | ||
| "default": "http://127.0.0.1:8081", | ||
| "description": "Base URL of the tapes core API.", | ||
| } | ||
| ], | ||
| } | ||
|
|
||
|
|
||
| class PromptRequest(BaseModel): | ||
| session_id: UUID | ||
|
|
||
|
|
||
| def create_app( | ||
| tapes_base_url: str | None = None, client: httpx.Client | None = None | ||
| ) -> FastAPI: | ||
| base_url = ( | ||
| tapes_base_url | ||
| or os.getenv("CASSETTE_TAPES_BASE_URL") | ||
| or "http://127.0.0.1:8081" | ||
| ).rstrip("/") | ||
| client = client or httpx.Client(timeout=10.0) | ||
| app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None) | ||
|
|
||
| @app.get("/ping") | ||
| def ping() -> dict[str, str]: | ||
| return {"status": "ok", "cassette": "prompt"} | ||
|
|
||
| @app.get("/openapi") | ||
| def openapi() -> JSONResponse: | ||
| return JSONResponse(OPENAPI) | ||
|
|
||
| def find_prompt(session_id: str) -> dict[str, str]: | ||
| try: | ||
| response = client.get( | ||
| f"{base_url}/v1/traces", params={"session_id": session_id} | ||
| ) | ||
| except httpx.HTTPError as error: | ||
| raise HTTPException(status_code=502, detail="tapes is unavailable") from error | ||
| if response.status_code == 404: | ||
| raise HTTPException(status_code=404, detail="session not found") | ||
| try: | ||
| response.raise_for_status() | ||
| items = response.json().get("items", []) | ||
| except (httpx.HTTPError, ValueError) as error: | ||
| raise HTTPException(status_code=502, detail="invalid response from tapes") from error | ||
| for item in items: | ||
| prompt = item.get("user_prompt", "") | ||
| if isinstance(prompt, str) and prompt: | ||
| return {"session_id": session_id, "prompt": prompt} | ||
| raise HTTPException(status_code=404, detail="prompt not found") | ||
|
|
||
| @app.get("/api/prompt/{session_id}") | ||
| def get_prompt(session_id: UUID) -> dict[str, str]: | ||
| return find_prompt(str(session_id)) | ||
|
|
||
| @app.post("/api/prompt/get") | ||
| def get_prompt_tool(request: PromptRequest) -> dict[str, str]: | ||
| return find_prompt(str(request.session_id)) | ||
|
|
||
| return app | ||
|
|
||
|
|
||
| OPENAPI: dict[str, Any] = { | ||
| "openapi": "3.1.0", | ||
| "info": { | ||
| "title": "Prompt Cassette", | ||
| "description": MANIFEST["cassette"]["description"], | ||
| "version": MANIFEST["cassette"]["version"], | ||
| }, | ||
| "x-tapes-cassette": MANIFEST, | ||
| "paths": { | ||
| "/api/prompt/get": { | ||
| "post": { | ||
| "operationId": "getPromptTool", | ||
| "summary": "Get a session's first user prompt", | ||
| "tags": ["prompt"], | ||
| "x-tapes-mcp": { | ||
| "name": "get_prompt", | ||
| "annotations": { | ||
| "readOnlyHint": True, | ||
| "idempotentHint": True, | ||
| "openWorldHint": False, | ||
| }, | ||
| }, | ||
| "requestBody": { | ||
| "required": True, | ||
| "content": { | ||
| "application/json": { | ||
| "schema": { | ||
| "type": "object", | ||
| "additionalProperties": False, | ||
| "required": ["session_id"], | ||
| "properties": { | ||
| "session_id": { | ||
| "type": "string", | ||
| "format": "uuid", | ||
| } | ||
| }, | ||
| } | ||
| } | ||
| }, | ||
| }, | ||
| "responses": { | ||
| "200": { | ||
| "description": "The prompt", | ||
| "content": { | ||
| "application/json": { | ||
| "schema": { | ||
| "type": "object", | ||
| "required": ["session_id", "prompt"], | ||
| "properties": { | ||
| "session_id": {"type": "string"}, | ||
| "prompt": {"type": "string"}, | ||
| }, | ||
| } | ||
| } | ||
| }, | ||
| }, | ||
| "404": {"description": "Session or prompt not found"}, | ||
| "502": {"description": "Tapes is unavailable"}, | ||
| }, | ||
| } | ||
| }, | ||
| "/api/prompt/{session_id}": { | ||
| "get": { | ||
| "operationId": "getPrompt", | ||
| "summary": "Get a session's first user prompt", | ||
| "tags": ["prompt"], | ||
| "parameters": [ | ||
| { | ||
| "name": "session_id", | ||
| "in": "path", | ||
| "required": True, | ||
| "schema": {"type": "string", "format": "uuid"}, | ||
| } | ||
| ], | ||
| "responses": { | ||
| "200": { | ||
| "description": "The prompt", | ||
| "content": { | ||
| "application/json": { | ||
| "schema": { | ||
| "type": "object", | ||
| "required": ["session_id", "prompt"], | ||
| "properties": { | ||
| "session_id": {"type": "string"}, | ||
| "prompt": {"type": "string"}, | ||
| }, | ||
| } | ||
| } | ||
| }, | ||
| }, | ||
| "422": {"description": "Invalid session id"}, | ||
| "404": {"description": "Session or prompt not found"}, | ||
| "502": {"description": "Tapes is unavailable"}, | ||
| }, | ||
| } | ||
| } | ||
| }, | ||
| } | ||
|
|
||
|
|
||
| def main() -> None: | ||
| host, _, port = os.getenv("CASSETTE_LISTEN", "127.0.0.1:9999").rpartition(":") | ||
| import uvicorn | ||
|
|
||
| uvicorn.run(create_app(), host=host or "127.0.0.1", port=int(port or 9999)) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| [project] | ||
| name = "prompt-cassette" | ||
| version = "0.1.0" | ||
| description = "Tapes cassette that returns a session's first user prompt" | ||
| readme = "README.md" | ||
| requires-python = ">=3.14" | ||
| dependencies = [ | ||
| "fastapi>=0.115", | ||
| "httpx>=0.27", | ||
| "uvicorn>=0.32", | ||
| ] | ||
|
|
||
| [dependency-groups] | ||
| dev = ["pytest>=8.3"] | ||
|
|
||
| [tool.pytest.ini_options] | ||
| pythonpath = ["."] |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A JSON-decodable 200 response such as
null,{"items": null}, or an array containing a non-object raises an uncaughtAttributeErrororTypeError, returning an internal 500 instead of the documented 502 for an invalid Tapes response.Knowledge Base Used: Session and trace API
Prompt To Fix With AI