Skip to content

馃Ч chore: Py example cassette - #331

Merged
jpmcb merged 1 commit into
mainfrom
cassette-anatomy
Aug 26, 2026
Merged

馃Ч chore: Py example cassette#331
jpmcb merged 1 commit into
mainfrom
cassette-anatomy

Conversation

@jpmcb

@jpmcb jpmcb commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Python tapes cassette example. Simple cassette, for some blog content.

Refs REL-118

@jpmcb
jpmcb force-pushed the cassette-anatomy branch from 40c65e1 to 2090658 Compare August 26, 2026 15:23
Signed-off-by: John McBride <john@papercompute.com>
@jpmcb
jpmcb force-pushed the cassette-anatomy branch from 2090658 to bb31b8a Compare August 26, 2026 15:26
@jpmcb
jpmcb marked this pull request as ready for review August 26, 2026 15:27
@linear-code

linear-code Bot commented Aug 26, 2026

Copy link
Copy Markdown

REL-118

@greptile-apps

greptile-apps Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a runnable Python cassette example that retrieves the first captured user prompt and exposes it through HTTP and MCP.

  • Adds a FastAPI cassette with embedded manifest and OpenAPI metadata.
  • Adds Docker Compose and container configuration for running the cassette with Tapes and PostgreSQL.
  • Adds locked Python dependencies, usage documentation, and basic contract tests.

Confidence Score: 4/5

The PR appears safe to merge, with a non-blocking response-validation gap that can turn a malformed successful upstream payload into an internal 500.

The cassette manifest, route mapping, MCP operation, container setup, and first-prompt selection align with current repository contracts; the only accepted issue concerns defensive handling of aberrant upstream JSON shapes.

Files Needing Attention: pkg/cassette/examples/py-example/main.py

Important Files Changed

Filename Overview
pkg/cassette/examples/py-example/main.py Implements the cassette service and contracts correctly overall, but malformed successful upstream payloads can escape as internal 500 responses.
pkg/cassette/examples/py-example/docker-compose.yaml Connects PostgreSQL, Tapes, and the cassette with consistent service URLs and retry-based cassette discovery.
pkg/cassette/examples/py-example/cassette.toml Declares deployment metadata equivalent to the manifest embedded in the service OpenAPI document.
pkg/cassette/examples/py-example/Dockerfile Builds the locked Python environment and runs the cassette as an unprivileged user.
pkg/cassette/examples/py-example/tests/test_main.py Covers the primary contract and input validation paths but does not exercise malformed upstream responses.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Tapes as Tapes API / MCP
    participant Cassette as Prompt cassette
    participant TraceAPI as Tapes trace API
    Client->>Tapes: Call prompt.get_prompt
    Tapes->>Cassette: POST /api/prompt/get
    Cassette->>TraceAPI: "GET /v1/traces?session_id=..."
    TraceAPI-->>Cassette: Ordered trace summaries
    Cassette-->>Tapes: First non-empty user prompt
    Tapes-->>Client: MCP structured result
Loading
Prompt To Fix All With AI
### Issue 1
pkg/cassette/examples/py-example/main.py:71-80
**Validate upstream response shapes**

A JSON-decodable 200 response such as `null`, `{"items": null}`, or an array containing a non-object raises an uncaught `AttributeError` or `TypeError`, returning an internal 500 instead of the documented 502 for an invalid Tapes response.

```suggestion
        try:
            response.raise_for_status()
            payload = response.json()
            if not isinstance(payload, dict):
                raise ValueError("response root is not an object")
            items = payload.get("items", [])
            if not isinstance(items, list):
                raise ValueError("response items is not an array")
            for item in items:
                if not isinstance(item, dict):
                    raise ValueError("response item is not an object")
                prompt = item.get("user_prompt", "")
                if isinstance(prompt, str) and prompt:
                    return {"session_id": session_id, "prompt": prompt}
        except (httpx.HTTPError, ValueError) as error:
            raise HTTPException(status_code=502, detail="invalid response from tapes") from error
        raise HTTPException(status_code=404, detail="prompt not found")
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "馃Ч chore: Py example cassette" | Re-trigger Greptile

Comment on lines +71 to +80
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")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Validate upstream response shapes

A JSON-decodable 200 response such as null, {"items": null}, or an array containing a non-object raises an uncaught AttributeError or TypeError, returning an internal 500 instead of the documented 502 for an invalid Tapes response.

Suggested change
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")
try:
response.raise_for_status()
payload = response.json()
if not isinstance(payload, dict):
raise ValueError("response root is not an object")
items = payload.get("items", [])
if not isinstance(items, list):
raise ValueError("response items is not an array")
for item in items:
if not isinstance(item, dict):
raise ValueError("response item is not an object")
prompt = item.get("user_prompt", "")
if isinstance(prompt, str) and prompt:
return {"session_id": session_id, "prompt": prompt}
except (httpx.HTTPError, ValueError) as error:
raise HTTPException(status_code=502, detail="invalid response from tapes") from error
raise HTTPException(status_code=404, detail="prompt not found")

Knowledge Base Used: Session and trace API

Prompt To Fix With AI
This is a comment left during a code review.
Path: pkg/cassette/examples/py-example/main.py
Line: 71-80

Comment:
**Validate upstream response shapes**

A JSON-decodable 200 response such as `null`, `{"items": null}`, or an array containing a non-object raises an uncaught `AttributeError` or `TypeError`, returning an internal 500 instead of the documented 502 for an invalid Tapes response.

```suggestion
        try:
            response.raise_for_status()
            payload = response.json()
            if not isinstance(payload, dict):
                raise ValueError("response root is not an object")
            items = payload.get("items", [])
            if not isinstance(items, list):
                raise ValueError("response items is not an array")
            for item in items:
                if not isinstance(item, dict):
                    raise ValueError("response item is not an object")
                prompt = item.get("user_prompt", "")
                if isinstance(prompt, str) and prompt:
                    return {"session_id": session_id, "prompt": prompt}
        except (httpx.HTTPError, ValueError) as error:
            raise HTTPException(status_code=502, detail="invalid response from tapes") from error
        raise HTTPException(status_code=404, detail="prompt not found")
```

**Knowledge Base Used:** [Session and trace API](https://app.greptile.com/paper-compute/-/custom-context/knowledge-base/papercomputeco/tapes/-/docs/session-and-trace-api.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@jpmcb
jpmcb merged commit 1659dfe into main Aug 26, 2026
8 checks passed
@jpmcb
jpmcb deleted the cassette-anatomy branch August 26, 2026 20:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant