Files
docs/standards/mcp/templates/app/infra/adapters/actions_stream.py
Vishesh 'ironeagle' Bangotra 0cb35d10a6 Phase 7: serve the vet sample from the template tools and docs
- build.py: _TEMPLATE_COMPONENTS rewritten around the vet sample (53 kinds —
  per-resource slices, jobs, worked appointment_reminder family, tests);
  seed tokens dropped from the manifest ('sample: vet' instead)
- registry.py: get_app_component(kind) no longer renames item/example seeds;
  prompts and tool docs updated to 'copy a slice and adapt'
- templates/README.md, 01_build_recipe, 40_ui_admin, wiki/03 updated off the
  old seed scheme; repo inventory/config now exclude vet (docs move under
  standards; react/vet itself is deleted in a later phase)
- regenerated MCP bundle, vendored templates, and site output
2026-10-20 14:58:17 +05:30

89 lines
2.9 KiB
Python

"""# Summary
Species-scoped action stream — the SSE event source behind ``stream_actions``.
---
Every pet gets a background asyncio task that emits random, species-appropriate
behaviors (a wagging dog, a kneading cat, a chirping bird) onto per-subscriber
queues. The HTTP handler subscribes one queue per connected client and
unsubscribes when the response closes.
Notes:
Process-local and best-effort — the stream is generated, not persisted,
and is intentionally independent of the persistence backend.
"""
import asyncio
import json
import random
_BEHAVIORS_BY_SPECIES: dict[str, list[str]] = {
"dog": ["wagging_tail", "stretching", "showing_belly", "barking", "panting"],
"cat": ["blep", "stretching", "showing_belly", "purring", "kneading"],
"bird": ["head_tilt", "stretching", "chirping", "wing_flap"],
}
_subscribers: dict[int, list[asyncio.Queue]] = {}
_worker_tasks: dict[int, asyncio.Task] = {}
async def _behavior_worker(pet_id: int, species: str) -> None:
"""Emit random behaviors for one pet until cancelled.
Args:
pet_id (int): Pet whose subscribers receive the events.
species (str): Animal species selecting the behavior pool.
"""
behaviors = _BEHAVIORS_BY_SPECIES.get(species, ["wagging_tail"])
while True:
action = random.choice(behaviors)
data = json.dumps({"action": action})
for q in _subscribers.get(pet_id, []):
await q.put(data)
await asyncio.sleep(random.uniform(1, 5))
def _ensure_worker(pet_id: int, species: str) -> None:
"""Start the behavior worker for ``pet_id`` if it is not running.
Args:
pet_id (int): Pet identifier.
species (str): Animal species selecting the behavior pool.
"""
if pet_id not in _worker_tasks or _worker_tasks[pet_id].done():
_subscribers.setdefault(pet_id, [])
_worker_tasks[pet_id] = asyncio.create_task(_behavior_worker(pet_id, species))
async def subscribe(pet_id: int, species: str) -> asyncio.Queue:
"""Subscribe a new listener queue for a pet's action stream.
Args:
pet_id (int): Pet identifier.
species (str): Animal species selecting the behavior pool.
Returns:
asyncio.Queue: Queue that receives JSON-encoded action payloads.
"""
q: asyncio.Queue = asyncio.Queue()
_subscribers.setdefault(pet_id, []).append(q)
_ensure_worker(pet_id, species)
return q
def unsubscribe(pet_id: int, q: asyncio.Queue) -> None:
"""Remove a listener queue and stop the worker when the last one leaves.
Args:
pet_id (int): Pet identifier.
q (asyncio.Queue): Queue returned by :func:`subscribe`.
"""
queues = _subscribers.get(pet_id, [])
if q in queues:
queues.remove(q)
if not queues:
task = _worker_tasks.pop(pet_id, None)
if task and not task.done():
task.cancel()
_subscribers.pop(pet_id, None)