SPB Git forge

spb/doc-api

Public
2commits 1branches 0releases
15.7 MBsize
maindefault branch
14 days agolast push
Python 88.3% TypeScript 7.6% Shell 4.1%
3.2 KB · 71 lines python
Raw Blame History
1#!/usr/bin/env python32"""Manual SSE parser (no SDK): accumulates text, tool input JSON and thinking; handles ping/error/unknown events.34STATUS: LIVE_VERIFIED 2026-09-18 (stdlib urllib via scripts/live.py). Run: .venv/bin/python examples/anthropic/streaming/manual_sse_parser.py5"""6import json7import sys8from pathlib import Path910sys.path.insert(0, str(Path(__file__).resolve().parents[3]))11from scripts.live import anthropic_request  # noqa: E402121314def parse_sse(lines):15    """Yield (event_name, data_dict). SSE frames are separated by blank lines; we only use event:/data: fields."""16    name, data = None, []17    for line in lines:18        if line == "":19            if data:20                yield name, json.loads("\n".join(data))21            name, data = None, []22        elif line.startswith("event:"):23            name = line[6:].strip()24        elif line.startswith("data:"):25            data.append(line[5:].strip())26    if data:27        yield name, json.loads("\n".join(data))282930body = {"model": "claude-haiku-4-5-20251001", "max_tokens": 64, "stream": True,31        "tools": [{"name": "get_weather", "description": "Weather for a city", "input_schema": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}],32        "tool_choice": {"type": "tool", "name": "get_weather"}, "messages": [{"role": "user", "content": "Weather in Paris?"}]}33status, lines, headers = anthropic_request("POST", "/v1/messages", body, stream=True, est_cost_usd=0.0009, note="example manual_sse_parser.py")34assert status == 200, status35blocks: dict[int, dict] = {}36message, order = {}, []37for name, data in parse_sse(lines):38    order.append(name)39    t = data.get("type")40    if t == "message_start":41        message = data["message"]42    elif t == "content_block_start":43        b = dict(data["content_block"]); b["_json"] = ""; blocks[data["index"]] = b44    elif t == "content_block_delta":45        d, b = data["delta"], blocks[data["index"]]46        if d["type"] == "text_delta": b["text"] += d["text"]47        elif d["type"] == "input_json_delta": b["_json"] += d["partial_json"]48        elif d["type"] == "thinking_delta": b["thinking"] += d["thinking"]49        elif d["type"] == "signature_delta": b["signature"] = d["signature"]50        elif d["type"] == "citations_delta": b.setdefault("citations", []).append(d["citation"])51    elif t == "content_block_stop":52        b = blocks[data["index"]]53        if b["type"] in ("tool_use", "server_tool_use"):54            b["input"] = json.loads(b.pop("_json") or "{}")   # guard the parse when using eager_input_streaming55        else:56            b.pop("_json", None)57    elif t == "message_delta":58        message.update({k: v for k, v in data["delta"].items()}); message["usage"].update(data["usage"])  # usage is cumulative59    elif t == "ping":60        pass61    elif t == "error":62        raise RuntimeError(data["error"])        # e.g. overloaded_error mid-stream63    elif t == "message_stop":64        break65    else:66        pass                                      # unknown event types must be ignored (versioning policy)67message["content"] = [blocks[i] for i in sorted(blocks)]68print("order :", " ".join(order))69print("stop  :", message["stop_reason"], "| usage:", message["usage"])70print("blocks:", json.dumps(message["content"])[:200])71