← Files CrowdStrike AgentWorksARCHIVED FILE

skills/invocation/scripts/stream_invocation.py

3.47 KB · Nov 11, 2026 · 13:09 UTC

↓ Download file

#!/usr/bin/env python3
"""
stream_invocation.py - Stream Charlotte AI AgentWorks invocation results.

Uses the native FalconPy Stream.stream_invocation_response_v1 method.

NOTE: This uses the same non-streaming request mechanism as every other FalconPy
call, so it does not provide true SSE token-by-token streaming. This script returns
the current/accumulated invocation payload as best-effort support.

Examples:
    python stream_invocation.py --id <invocation-uuid>
"""

import argparse
import json
import sys
import os

sys.path.insert(
    0,
    os.path.join(
        os.path.dirname(os.path.realpath(__file__)),
        "..", "..", "..", "common", "scripts",
    ),
)
import _bootstrap

_bootstrap.ensure_deps(__file__)
from auth import call_native, get_stream_client


def parse_sse(raw: bytes | str) -> list[dict]:
    """Parse a server-sent-event payload into a list of {"event", "data"} dicts.

    Each event's `data:` lines are joined and decoded as JSON when possible, otherwise kept as
    text. The terminal `[DONE]` sentinel is dropped.
    """
    text = raw.decode("utf-8", errors="replace") if isinstance(raw, bytes) else raw
    events = []
    for block in text.replace("\r\n", "\n").split("\n\n"):
        event_name = "message"
        data_lines = []
        for line in block.split("\n"):
            if line.startswith("event:"):
                event_name = line[len("event:"):].strip()
            elif line.startswith("data:"):
                data_lines.append(line[len("data:"):].lstrip())
        if not data_lines:
            continue
        data = "\n".join(data_lines)
        if data == "[DONE]":
            continue
        try:
            data = json.loads(data)
        except ValueError:
            pass
        events.append({"event": event_name, "data": data})
    return events


def main() -> None:
    parser = argparse.ArgumentParser(description="Stream invocation results")
    parser.add_argument("--id", required=True, help="Invocation ID")
    parser.add_argument("--json", action="store_true", help="Output raw JSON")
    args = parser.parse_args()

    # Swagger documents this query param as singular 'id', not 'ids'.
    params = {"id": args.id}

    # The stream endpoint returns the raw server-sent-event text, not a JSON object.
    response = call_native(get_stream_client().stream_invocation_response_v1, parameters=params)
    events = parse_sse(response) if isinstance(response, (bytes, str)) else []

    if args.json:
        print(json.dumps(events if events else response, indent=2, default=str))
        return

    if not events:
        print("No streamed messages for given ID (it may not exist or has produced no output yet).")
        return

    print(f"Invocation: {args.id}")
    print(f"Events ({len(events)}):\n")
    for i, event in enumerate(events, 1):
        data = event["data"]
        if isinstance(data, dict):
            role = (data.get("role") or event["event"]).upper()
            content = data.get("content") or ""
            if not isinstance(content, str):
                content = json.dumps(content)
        else:
            role, content = event["event"].upper(), str(data)
        print(f"{i}. [{role}]")
        print(f"   {content[:200]}{'...' if len(content) > 200 else ''}")
        print()


if __name__ == "__main__":
    try:
        main()
    except RuntimeError as exc:
        # call_native raises RuntimeError on HTTP errors (e.g. 404 bad ID, 403 missing scope).
        print(f"ERROR: {exc}", file=sys.stderr)
        sys.exit(1)

SHA-256: 7737965386a3427a6063d149f4b0953dcd3c12c25ef1b429fa74eb9b95ce6be0