|
23 | 23 | import asyncio |
24 | 24 | import json |
25 | 25 | import os |
| 26 | +import queue as _queue |
| 27 | +import threading |
26 | 28 | import time |
27 | 29 | import traceback |
28 | 30 | from concurrent.futures import ThreadPoolExecutor, as_completed |
29 | | -from typing import Any, Callable, List, Optional, Union |
| 31 | +from typing import Any, Callable, Dict, List, Optional, Union |
30 | 32 |
|
31 | 33 | from loguru import logger |
32 | 34 | from pydantic import BaseModel, Field |
@@ -1528,12 +1530,18 @@ def agent_streaming_callback(chunk: str): |
1528 | 1530 | f"{error_msg}\n[TRACE] Traceback: {traceback.format_exc()}" |
1529 | 1531 | ) |
1530 | 1532 |
|
1531 | | - output = agent.run( |
1532 | | - task=f"History: {self.conversation.get_str()} \n\n Task: {task}", |
1533 | | - streaming_callback=agent_streaming_callback, |
1534 | | - *args, |
1535 | | - **kwargs, |
1536 | | - ) |
| 1533 | + # Temporarily enable streaming so call_llm honours the callback |
| 1534 | + original_streaming_on = getattr(agent, "streaming_on", False) |
| 1535 | + agent.streaming_on = True |
| 1536 | + try: |
| 1537 | + output = agent.run( |
| 1538 | + task=f"History: {self.conversation.get_str()} \n\n Task: {task}", |
| 1539 | + streaming_callback=agent_streaming_callback, |
| 1540 | + *args, |
| 1541 | + **kwargs, |
| 1542 | + ) |
| 1543 | + finally: |
| 1544 | + agent.streaming_on = original_streaming_on |
1537 | 1545 |
|
1538 | 1546 | # Call completion callback |
1539 | 1547 | try: |
@@ -1894,3 +1902,165 @@ async def arun( |
1894 | 1902 | *args, |
1895 | 1903 | **kwargs, |
1896 | 1904 | ) |
| 1905 | + |
| 1906 | + async def arun_stream( |
| 1907 | + self, |
| 1908 | + task: Optional[str] = None, |
| 1909 | + img: Optional[str] = None, |
| 1910 | + with_events: bool = False, |
| 1911 | + **kwargs, |
| 1912 | + ): |
| 1913 | + """Async generator that streams tokens from worker agents. |
| 1914 | +
|
| 1915 | + Bridges the existing callback-based streaming in ``run()`` to an |
| 1916 | + async generator using an ``asyncio.Queue``. The synchronous |
| 1917 | + ``run()`` executes in a background thread; each token callback |
| 1918 | + pushes an item to the queue which this generator yields. |
| 1919 | +
|
| 1920 | + Args: |
| 1921 | + task: The task to be processed by the swarm. |
| 1922 | + img: Optional image input for the agents. |
| 1923 | + with_events: When False (default), yield ``(agent_name, token)`` |
| 1924 | + tuples. When True, yield structured event dicts: |
| 1925 | + ``{"type": "agent_start", "agent": ...}``, |
| 1926 | + ``{"type": "token", "agent": ..., "token": ...}``, |
| 1927 | + ``{"type": "agent_end", "agent": ...}``. |
| 1928 | +
|
| 1929 | + Yields: |
| 1930 | + tuple | dict: Per-token streaming items. |
| 1931 | + """ |
| 1932 | + q: asyncio.Queue = asyncio.Queue() |
| 1933 | + DONE = object() |
| 1934 | + ERROR = object() |
| 1935 | + loop = asyncio.get_running_loop() |
| 1936 | + |
| 1937 | + # Track which agents have started and accumulate their tokens |
| 1938 | + agent_chunks: Dict[str, List[str]] = {} |
| 1939 | + |
| 1940 | + def _streaming_callback( |
| 1941 | + agent_name: str, chunk: str, is_final: bool |
| 1942 | + ): |
| 1943 | + loop.call_soon_threadsafe( |
| 1944 | + q.put_nowait, (agent_name, chunk, is_final) |
| 1945 | + ) |
| 1946 | + |
| 1947 | + async def _run_in_thread(): |
| 1948 | + try: |
| 1949 | + await asyncio.to_thread( |
| 1950 | + self.run, |
| 1951 | + task=task, |
| 1952 | + img=img, |
| 1953 | + streaming_callback=_streaming_callback, |
| 1954 | + **kwargs, |
| 1955 | + ) |
| 1956 | + except Exception as e: |
| 1957 | + loop.call_soon_threadsafe( |
| 1958 | + q.put_nowait, (ERROR, e, False) |
| 1959 | + ) |
| 1960 | + finally: |
| 1961 | + loop.call_soon_threadsafe( |
| 1962 | + q.put_nowait, (DONE, None, False) |
| 1963 | + ) |
| 1964 | + |
| 1965 | + runner = asyncio.create_task(_run_in_thread()) |
| 1966 | + |
| 1967 | + try: |
| 1968 | + while True: |
| 1969 | + item = await q.get() |
| 1970 | + kind, payload, is_final = item |
| 1971 | + |
| 1972 | + if kind is DONE: |
| 1973 | + break |
| 1974 | + if kind is ERROR: |
| 1975 | + raise payload |
| 1976 | + |
| 1977 | + agent_name: str = kind |
| 1978 | + chunk: str = payload |
| 1979 | + |
| 1980 | + if agent_name not in agent_chunks: |
| 1981 | + agent_chunks[agent_name] = [] |
| 1982 | + if with_events: |
| 1983 | + yield { |
| 1984 | + "type": "agent_start", |
| 1985 | + "agent": agent_name, |
| 1986 | + } |
| 1987 | + |
| 1988 | + if is_final: |
| 1989 | + agent_output = "".join(agent_chunks.pop(agent_name, [])) |
| 1990 | + if with_events: |
| 1991 | + yield { |
| 1992 | + "type": "agent_end", |
| 1993 | + "agent": agent_name, |
| 1994 | + "output": agent_output, |
| 1995 | + } |
| 1996 | + else: |
| 1997 | + if chunk: |
| 1998 | + agent_chunks[agent_name].append(chunk) |
| 1999 | + if with_events: |
| 2000 | + yield { |
| 2001 | + "type": "token", |
| 2002 | + "agent": agent_name, |
| 2003 | + "token": chunk, |
| 2004 | + } |
| 2005 | + else: |
| 2006 | + yield (agent_name, chunk) |
| 2007 | + finally: |
| 2008 | + await runner |
| 2009 | + |
| 2010 | + def run_stream( |
| 2011 | + self, |
| 2012 | + task: Optional[str] = None, |
| 2013 | + img: Optional[str] = None, |
| 2014 | + with_events: bool = False, |
| 2015 | + **kwargs, |
| 2016 | + ): |
| 2017 | + """Sync generator version of ``arun_stream``. |
| 2018 | +
|
| 2019 | + Bridges the async generator to a sync iterator using a thread |
| 2020 | + and a ``queue.Queue``. Use ``arun_stream`` directly when you |
| 2021 | + already have a running event loop (e.g. inside a FastAPI handler). |
| 2022 | +
|
| 2023 | + Args: |
| 2024 | + task: The task to be processed by the swarm. |
| 2025 | + img: Optional image input for the agents. |
| 2026 | + with_events: When False (default), yield ``(agent_name, token)`` |
| 2027 | + tuples. When True, yield structured event dicts. |
| 2028 | +
|
| 2029 | + Yields: |
| 2030 | + tuple | dict: Per-token streaming items. |
| 2031 | + """ |
| 2032 | + sync_q: _queue.Queue = _queue.Queue() |
| 2033 | + DONE = object() |
| 2034 | + exc_holder: List[Optional[Exception]] = [None] |
| 2035 | + |
| 2036 | + def _runner(): |
| 2037 | + async def _consume(): |
| 2038 | + async for evt in self.arun_stream( |
| 2039 | + task=task, |
| 2040 | + img=img, |
| 2041 | + with_events=with_events, |
| 2042 | + **kwargs, |
| 2043 | + ): |
| 2044 | + sync_q.put(evt) |
| 2045 | + |
| 2046 | + try: |
| 2047 | + asyncio.run(_consume()) |
| 2048 | + except Exception as e: |
| 2049 | + exc_holder[0] = e |
| 2050 | + finally: |
| 2051 | + sync_q.put(DONE) |
| 2052 | + |
| 2053 | + t = threading.Thread(target=_runner, daemon=True) |
| 2054 | + t.start() |
| 2055 | + |
| 2056 | + try: |
| 2057 | + while True: |
| 2058 | + item = sync_q.get() |
| 2059 | + if item is DONE: |
| 2060 | + break |
| 2061 | + yield item |
| 2062 | + finally: |
| 2063 | + t.join(timeout=5) |
| 2064 | + |
| 2065 | + if exc_holder[0] is not None: |
| 2066 | + raise exc_holder[0] |
0 commit comments