yapwrap 0.0.1a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- yapwrap/__init__.py +36 -0
- yapwrap/agents/__init__.py +30 -0
- yapwrap/agents/agno.py +54 -0
- yapwrap/agents/callable.py +57 -0
- yapwrap/agents/claude.py +49 -0
- yapwrap/agents/crewai.py +25 -0
- yapwrap/agents/google_adk.py +47 -0
- yapwrap/agents/langgraph.py +59 -0
- yapwrap/agents/llamaindex.py +27 -0
- yapwrap/agents/microsoft_agent.py +20 -0
- yapwrap/agents/openai_agents.py +34 -0
- yapwrap/agents/pydantic_ai.py +27 -0
- yapwrap/agents/smolagents.py +37 -0
- yapwrap/core/__init__.py +27 -0
- yapwrap/core/audio.py +168 -0
- yapwrap/core/base.py +113 -0
- yapwrap/core/events.py +57 -0
- yapwrap/core/memory.py +73 -0
- yapwrap/core/phrases.py +89 -0
- yapwrap/core/protocols.py +18 -0
- yapwrap/core/session.py +256 -0
- yapwrap/core/yapwrap.py +113 -0
- yapwrap/stt/__init__.py +24 -0
- yapwrap/stt/assemblyai.py +68 -0
- yapwrap/stt/deepgram.py +98 -0
- yapwrap/stt/elevenlabs.py +63 -0
- yapwrap/stt/gladia.py +77 -0
- yapwrap/stt/google_chirp.py +66 -0
- yapwrap/stt/openai_realtime.py +85 -0
- yapwrap/stt/soniox.py +80 -0
- yapwrap/stt/speechmatics.py +81 -0
- yapwrap/transports/__init__.py +14 -0
- yapwrap/transports/local.py +121 -0
- yapwrap/transports/webrtc.py +132 -0
- yapwrap/transports/websocket.py +86 -0
- yapwrap/tts/__init__.py +28 -0
- yapwrap/tts/azure.py +59 -0
- yapwrap/tts/cartesia.py +77 -0
- yapwrap/tts/deepgram.py +57 -0
- yapwrap/tts/elevenlabs.py +69 -0
- yapwrap/tts/gemini.py +62 -0
- yapwrap/tts/hume.py +62 -0
- yapwrap/tts/inworld.py +58 -0
- yapwrap/tts/openai.py +61 -0
- yapwrap/tts/rime.py +64 -0
- yapwrap/tts/sarvam.py +71 -0
- yapwrap/vad/__init__.py +18 -0
- yapwrap/vad/cobra.py +70 -0
- yapwrap/vad/energy.py +55 -0
- yapwrap/vad/silero.py +74 -0
- yapwrap/vad/smart_turn.py +55 -0
- yapwrap/vad/webrtc.py +72 -0
- yapwrap-0.0.1a1.dist-info/METADATA +339 -0
- yapwrap-0.0.1a1.dist-info/RECORD +56 -0
- yapwrap-0.0.1a1.dist-info/WHEEL +4 -0
- yapwrap-0.0.1a1.dist-info/licenses/LICENSE +201 -0
yapwrap/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""YapWrap: Framework-agnostic, ultra-low-latency streaming voice pipeline for AI agents."""
|
|
2
|
+
|
|
3
|
+
from yapwrap.core.base import BaseAgent, BaseSTT, BaseTTS, BaseVAD, BaseTransport
|
|
4
|
+
from yapwrap.core.yapwrap import YapWrap
|
|
5
|
+
from yapwrap.core.protocols import Transport, VAD, STT, Agent, TTS, SessionEventSink
|
|
6
|
+
from yapwrap.core.events import AgentEvent, SessionEvent, TranscriptEvent
|
|
7
|
+
from yapwrap.core.session import VoiceSession
|
|
8
|
+
from yapwrap.core.memory import InMemoryHistory, ConversationTurn
|
|
9
|
+
|
|
10
|
+
# Yapper is an alias for YapWrap
|
|
11
|
+
Yapper = YapWrap
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"BaseAgent",
|
|
15
|
+
"BaseSTT",
|
|
16
|
+
"BaseTTS",
|
|
17
|
+
"BaseVAD",
|
|
18
|
+
"BaseTransport",
|
|
19
|
+
"YapWrap",
|
|
20
|
+
"Yapper",
|
|
21
|
+
"Transport",
|
|
22
|
+
"VAD",
|
|
23
|
+
"STT",
|
|
24
|
+
"Agent",
|
|
25
|
+
"TTS",
|
|
26
|
+
"AgentEvent",
|
|
27
|
+
"SessionEvent",
|
|
28
|
+
"SessionEventSink",
|
|
29
|
+
"TranscriptEvent",
|
|
30
|
+
"VoiceSession",
|
|
31
|
+
"InMemoryHistory",
|
|
32
|
+
"ConversationTurn",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
__version__ = "0.0.1a1"
|
|
36
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Python Agent SDK adapters for yapwrap."""
|
|
2
|
+
|
|
3
|
+
from yapwrap.core.base import BaseAgent
|
|
4
|
+
from yapwrap.agents.agno import AgnoAgent
|
|
5
|
+
from yapwrap.agents.langgraph import LangGraphAgent
|
|
6
|
+
from yapwrap.agents.openai_agents import OpenAIAgentsSDKAgent
|
|
7
|
+
from yapwrap.agents.pydantic_ai import PydanticAIAgent
|
|
8
|
+
from yapwrap.agents.google_adk import GoogleADKAgent
|
|
9
|
+
from yapwrap.agents.claude import ClaudeAgentSDKAgent
|
|
10
|
+
from yapwrap.agents.crewai import CrewAIAgent
|
|
11
|
+
from yapwrap.agents.llamaindex import LlamaIndexAgent
|
|
12
|
+
from yapwrap.agents.microsoft_agent import MicrosoftAgentFrameworkAgent
|
|
13
|
+
from yapwrap.agents.smolagents import SmolAgentsAgent
|
|
14
|
+
from yapwrap.agents.callable import CallableAgent
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"BaseAgent",
|
|
18
|
+
"AgnoAgent",
|
|
19
|
+
"LangGraphAgent",
|
|
20
|
+
"OpenAIAgentsSDKAgent",
|
|
21
|
+
"PydanticAIAgent",
|
|
22
|
+
"GoogleADKAgent",
|
|
23
|
+
"ClaudeAgentSDKAgent",
|
|
24
|
+
"CrewAIAgent",
|
|
25
|
+
"LlamaIndexAgent",
|
|
26
|
+
"MicrosoftAgentFrameworkAgent",
|
|
27
|
+
"SmolAgentsAgent",
|
|
28
|
+
"CallableAgent",
|
|
29
|
+
]
|
|
30
|
+
|
yapwrap/agents/agno.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Agno Agent adapter for yapwrap."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
from typing import Any, AsyncIterator
|
|
6
|
+
|
|
7
|
+
from yapwrap.core.base import BaseAgent
|
|
8
|
+
from yapwrap.core.events import AgentEvent
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AgnoAgent(BaseAgent):
|
|
12
|
+
"""Adapts an Agno Agent into a yapwrap Agent."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, agent: Any, **run_kwargs: Any) -> None:
|
|
15
|
+
self.agent = agent
|
|
16
|
+
self.run_kwargs = run_kwargs
|
|
17
|
+
|
|
18
|
+
async def respond(self, text: str) -> AsyncIterator[AgentEvent]:
|
|
19
|
+
"""Stream events from an Agno agent run."""
|
|
20
|
+
# Agno agents support async streaming with stream=True
|
|
21
|
+
if hasattr(self.agent, "arun"):
|
|
22
|
+
stream = self.agent.arun(text, **{**self.run_kwargs, "stream": True, "stream_events": True})
|
|
23
|
+
if inspect.isawaitable(stream):
|
|
24
|
+
stream = await stream
|
|
25
|
+
try:
|
|
26
|
+
async for event in stream:
|
|
27
|
+
kind = getattr(event, "event", "")
|
|
28
|
+
content = getattr(event, "content", None)
|
|
29
|
+
|
|
30
|
+
if kind == "RunContent" and isinstance(content, str):
|
|
31
|
+
yield AgentEvent.text(content)
|
|
32
|
+
elif kind in ("ToolCallStarted", "ToolCallCompleted"):
|
|
33
|
+
tool = getattr(event, "tool", None)
|
|
34
|
+
name = getattr(tool, "tool_name", "tool")
|
|
35
|
+
if kind == "ToolCallStarted":
|
|
36
|
+
yield AgentEvent.tool_start(name, getattr(tool, "tool_args", None))
|
|
37
|
+
else:
|
|
38
|
+
yield AgentEvent.tool_end(name, getattr(tool, "result", None))
|
|
39
|
+
elif kind in ("RunError", "RunCancelled"):
|
|
40
|
+
raise RuntimeError(f"Agno {kind}: {content or ''}")
|
|
41
|
+
elif isinstance(content, str) and content and not kind:
|
|
42
|
+
yield AgentEvent.text(content)
|
|
43
|
+
finally:
|
|
44
|
+
close = getattr(stream, "aclose", None)
|
|
45
|
+
if close is not None:
|
|
46
|
+
await close()
|
|
47
|
+
elif hasattr(self.agent, "run"):
|
|
48
|
+
# Synchronous fallback
|
|
49
|
+
resp = await asyncio.to_thread(self.agent.run, text, **self.run_kwargs)
|
|
50
|
+
content = getattr(resp, "content", str(resp))
|
|
51
|
+
yield AgentEvent.text(content)
|
|
52
|
+
else:
|
|
53
|
+
raise TypeError("AgnoAgent expects an object with arun() or run()")
|
|
54
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Adapt plain Python callables without importing an agent framework."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
from collections.abc import AsyncIterable, Iterable
|
|
6
|
+
from typing import Any, AsyncIterator, Callable
|
|
7
|
+
|
|
8
|
+
from yapwrap.core.base import BaseAgent
|
|
9
|
+
from yapwrap.core.events import AgentEvent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CallableAgent(BaseAgent):
|
|
13
|
+
"""Accept text, events, and synchronous or asynchronous streams of either.
|
|
14
|
+
|
|
15
|
+
Async callables are preferred for cancellable network work. Synchronous
|
|
16
|
+
callables run in a worker thread; Python cannot interrupt that thread.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, func: Callable[[str], Any]) -> None:
|
|
20
|
+
self.func = func
|
|
21
|
+
|
|
22
|
+
async def respond(self, text: str) -> AsyncIterator[AgentEvent]:
|
|
23
|
+
if inspect.iscoroutinefunction(self.func) or inspect.isasyncgenfunction(self.func):
|
|
24
|
+
result = self.func(text)
|
|
25
|
+
else:
|
|
26
|
+
result = await asyncio.to_thread(self.func, text)
|
|
27
|
+
if inspect.isawaitable(result):
|
|
28
|
+
result = await result
|
|
29
|
+
if isinstance(result, (str, AgentEvent)):
|
|
30
|
+
yield self._event(result)
|
|
31
|
+
elif isinstance(result, AsyncIterable):
|
|
32
|
+
stream = result.__aiter__()
|
|
33
|
+
try:
|
|
34
|
+
async for item in stream:
|
|
35
|
+
yield self._event(item)
|
|
36
|
+
finally:
|
|
37
|
+
close = getattr(stream, "aclose", None)
|
|
38
|
+
if close is not None:
|
|
39
|
+
await close()
|
|
40
|
+
elif isinstance(result, Iterable):
|
|
41
|
+
iterator = iter(result)
|
|
42
|
+
end = object()
|
|
43
|
+
while True:
|
|
44
|
+
item = await asyncio.to_thread(next, iterator, end)
|
|
45
|
+
if item is end:
|
|
46
|
+
break
|
|
47
|
+
yield self._event(item)
|
|
48
|
+
else:
|
|
49
|
+
raise TypeError("CallableAgent must return text, AgentEvent, or an iterable of either")
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _event(item: Any) -> AgentEvent:
|
|
53
|
+
if isinstance(item, AgentEvent):
|
|
54
|
+
return item
|
|
55
|
+
if isinstance(item, str):
|
|
56
|
+
return AgentEvent.text(item)
|
|
57
|
+
raise TypeError("Agent stream items must be strings or AgentEvent objects")
|
yapwrap/agents/claude.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Claude Agent SDK client adapter."""
|
|
2
|
+
|
|
3
|
+
from contextlib import aclosing
|
|
4
|
+
from typing import Any, AsyncIterator
|
|
5
|
+
from yapwrap.core.base import BaseAgent
|
|
6
|
+
from yapwrap.core.events import AgentEvent
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ClaudeAgentSDKAgent(BaseAgent):
|
|
10
|
+
"""Adapt a connected ClaudeSDKClient owned by an async context manager."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, agent: Any) -> None:
|
|
13
|
+
self.agent = agent
|
|
14
|
+
|
|
15
|
+
async def respond(self, text: str) -> AsyncIterator[AgentEvent]:
|
|
16
|
+
await self.agent.query(text)
|
|
17
|
+
finished = False
|
|
18
|
+
partial = False
|
|
19
|
+
try:
|
|
20
|
+
async with aclosing(self.agent.receive_response()) as stream:
|
|
21
|
+
async for message in stream:
|
|
22
|
+
kind = type(message).__name__
|
|
23
|
+
if kind == "StreamEvent":
|
|
24
|
+
event = message.event
|
|
25
|
+
if event.get("type") == "message_start":
|
|
26
|
+
partial = False
|
|
27
|
+
delta = event.get("delta", {})
|
|
28
|
+
if delta.get("type") == "text_delta":
|
|
29
|
+
partial = True
|
|
30
|
+
if delta.get("text"):
|
|
31
|
+
yield AgentEvent.text(delta["text"])
|
|
32
|
+
elif kind == "AssistantMessage":
|
|
33
|
+
if not partial:
|
|
34
|
+
for block in message.content:
|
|
35
|
+
if type(block).__name__ == "TextBlock" and block.text:
|
|
36
|
+
yield AgentEvent.text(block.text)
|
|
37
|
+
partial = False
|
|
38
|
+
elif kind == "ResultMessage":
|
|
39
|
+
finished = True
|
|
40
|
+
if message.is_error:
|
|
41
|
+
raise RuntimeError(message.result or "Claude agent run failed")
|
|
42
|
+
finished = True
|
|
43
|
+
finally:
|
|
44
|
+
if not finished:
|
|
45
|
+
await self.agent.interrupt()
|
|
46
|
+
# Drain the interrupted run before the next query.
|
|
47
|
+
async for _ in self.agent.receive_response():
|
|
48
|
+
pass
|
|
49
|
+
|
yapwrap/agents/crewai.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""CrewAI Crew adapter."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any, AsyncIterator
|
|
5
|
+
from yapwrap.core.base import BaseAgent
|
|
6
|
+
from yapwrap.core.events import AgentEvent
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CrewAIAgent(BaseAgent):
|
|
10
|
+
"""Speak a Crew's final output. Tasks receive the utterance as {input}."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, crew_or_agent: Any) -> None:
|
|
13
|
+
self.crew = crew_or_agent
|
|
14
|
+
|
|
15
|
+
async def respond(self, text: str) -> AsyncIterator[AgentEvent]:
|
|
16
|
+
inputs = {"input": text}
|
|
17
|
+
if hasattr(self.crew, "akickoff"):
|
|
18
|
+
result = await self.crew.akickoff(inputs=inputs)
|
|
19
|
+
elif hasattr(self.crew, "kickoff_async"):
|
|
20
|
+
result = await self.crew.kickoff_async(inputs=inputs)
|
|
21
|
+
else:
|
|
22
|
+
result = await asyncio.to_thread(self.crew.kickoff, inputs=inputs)
|
|
23
|
+
if result.raw:
|
|
24
|
+
yield AgentEvent.text(result.raw)
|
|
25
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Google ADK Runner adapter."""
|
|
2
|
+
|
|
3
|
+
from contextlib import aclosing
|
|
4
|
+
from typing import Any, AsyncIterator
|
|
5
|
+
from yapwrap.core.base import BaseAgent
|
|
6
|
+
from yapwrap.core.events import AgentEvent
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GoogleADKAgent(BaseAgent):
|
|
10
|
+
"""Adapt a Runner and an existing ADK session, owned by the application."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, runner: Any, *, user_id: str, session_id: str, run_config: Any = None) -> None:
|
|
13
|
+
self.runner = runner
|
|
14
|
+
self.user_id = user_id
|
|
15
|
+
self.session_id = session_id
|
|
16
|
+
self.run_config = run_config
|
|
17
|
+
|
|
18
|
+
async def respond(self, text: str) -> AsyncIterator[AgentEvent]:
|
|
19
|
+
from google.genai import types
|
|
20
|
+
|
|
21
|
+
message = types.Content(role="user", parts=[types.Part(text=text)])
|
|
22
|
+
stream = self.runner.run_async(
|
|
23
|
+
user_id=self.user_id, session_id=self.session_id,
|
|
24
|
+
new_message=message, run_config=self.run_config,
|
|
25
|
+
)
|
|
26
|
+
partial = False
|
|
27
|
+
async with aclosing(stream):
|
|
28
|
+
async for event in stream:
|
|
29
|
+
if getattr(event, "error_code", None):
|
|
30
|
+
raise RuntimeError(event.error_message or event.error_code)
|
|
31
|
+
content = getattr(event, "content", None)
|
|
32
|
+
if content is None or content.role == "user":
|
|
33
|
+
continue
|
|
34
|
+
output = "".join(
|
|
35
|
+
part.text for part in (content.parts or [])
|
|
36
|
+
if part.text and not getattr(part, "thought", False)
|
|
37
|
+
)
|
|
38
|
+
if event.partial:
|
|
39
|
+
partial = True
|
|
40
|
+
if output:
|
|
41
|
+
yield AgentEvent.text(output)
|
|
42
|
+
else:
|
|
43
|
+
# ADK emits an aggregate after partial tokens of each response.
|
|
44
|
+
if output and not partial:
|
|
45
|
+
yield AgentEvent.text(output)
|
|
46
|
+
partial = False
|
|
47
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""LangGraph and LangChain create_agent adapter."""
|
|
2
|
+
|
|
3
|
+
from contextlib import aclosing
|
|
4
|
+
from typing import Any, AsyncIterator
|
|
5
|
+
from yapwrap.core.base import BaseAgent
|
|
6
|
+
from yapwrap.core.events import AgentEvent
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _text(content: Any) -> str:
|
|
10
|
+
if isinstance(content, str):
|
|
11
|
+
return content
|
|
12
|
+
if isinstance(content, list):
|
|
13
|
+
return "".join(
|
|
14
|
+
block if isinstance(block, str) else block.get("text", "")
|
|
15
|
+
for block in content
|
|
16
|
+
if isinstance(block, str) or isinstance(block, dict) and block.get("type") == "text"
|
|
17
|
+
)
|
|
18
|
+
return ""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LangGraphAgent(BaseAgent):
|
|
22
|
+
"""Adapt a compiled graph, optionally passing config (e.g. thread_id)."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, graph: Any, **run_kwargs: Any) -> None:
|
|
25
|
+
self.graph = graph
|
|
26
|
+
self.run_kwargs = run_kwargs
|
|
27
|
+
|
|
28
|
+
async def respond(self, text: str) -> AsyncIterator[AgentEvent]:
|
|
29
|
+
inputs = {"messages": [{"role": "user", "content": text}]}
|
|
30
|
+
if hasattr(self.graph, "astream_events"):
|
|
31
|
+
stream = self.graph.astream_events(inputs, **{**self.run_kwargs, "version": "v2"})
|
|
32
|
+
async with aclosing(stream):
|
|
33
|
+
async for event in stream:
|
|
34
|
+
kind, data = event.get("event"), event.get("data", {})
|
|
35
|
+
if kind == "on_chat_model_stream":
|
|
36
|
+
content = _text(getattr(data.get("chunk"), "content", ""))
|
|
37
|
+
if content:
|
|
38
|
+
yield AgentEvent.text(content)
|
|
39
|
+
elif kind == "on_tool_start":
|
|
40
|
+
yield AgentEvent.tool_start(event.get("name", "tool"), data.get("input"))
|
|
41
|
+
elif kind == "on_tool_end":
|
|
42
|
+
yield AgentEvent.tool_end(event.get("name", "tool"), data.get("output"))
|
|
43
|
+
return # Never execute tools again as a fallback after an error.
|
|
44
|
+
|
|
45
|
+
async with aclosing(self.graph.astream(inputs, **self.run_kwargs)) as stream:
|
|
46
|
+
async for chunk in stream:
|
|
47
|
+
if isinstance(chunk, str):
|
|
48
|
+
yield AgentEvent.text(chunk)
|
|
49
|
+
continue
|
|
50
|
+
states = [chunk] if "messages" in chunk else chunk.values()
|
|
51
|
+
for state in states:
|
|
52
|
+
if not isinstance(state, dict):
|
|
53
|
+
continue
|
|
54
|
+
for message in state.get("messages", []):
|
|
55
|
+
role = message.get("role") if isinstance(message, dict) else getattr(message, "type", None)
|
|
56
|
+
if role in ("assistant", "ai"):
|
|
57
|
+
content = _text(message.get("content") if isinstance(message, dict) else message.content)
|
|
58
|
+
if content:
|
|
59
|
+
yield AgentEvent.text(content)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""LlamaIndex FunctionAgent / AgentWorkflow adapter."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, AsyncIterator
|
|
4
|
+
from yapwrap.core.base import BaseAgent
|
|
5
|
+
from yapwrap.core.events import AgentEvent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LlamaIndexAgent(BaseAgent):
|
|
9
|
+
"""Adapt a workflow; pass ctx=Context(agent) to retain conversation state."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, agent: Any, **run_kwargs: Any) -> None:
|
|
12
|
+
self.agent = agent
|
|
13
|
+
self.run_kwargs = run_kwargs
|
|
14
|
+
|
|
15
|
+
async def respond(self, text: str) -> AsyncIterator[AgentEvent]:
|
|
16
|
+
handler = self.agent.run(user_msg=text, **self.run_kwargs)
|
|
17
|
+
finished = False
|
|
18
|
+
try:
|
|
19
|
+
async for event in handler.stream_events():
|
|
20
|
+
if type(event).__name__ == "AgentStream" and event.delta:
|
|
21
|
+
yield AgentEvent.text(event.delta)
|
|
22
|
+
await handler # Surface failures without repeating the final text.
|
|
23
|
+
finished = True
|
|
24
|
+
finally:
|
|
25
|
+
if not finished:
|
|
26
|
+
await handler.cancel_run()
|
|
27
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Microsoft Agent Framework adapter."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, AsyncIterator
|
|
4
|
+
from yapwrap.core.base import BaseAgent
|
|
5
|
+
from yapwrap.core.events import AgentEvent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MicrosoftAgentFrameworkAgent(BaseAgent):
|
|
9
|
+
"""Adapt an Agent's ResponseStream, closing it when speech is interrupted."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, agent: Any, **run_kwargs: Any) -> None:
|
|
12
|
+
self.agent = agent
|
|
13
|
+
self.run_kwargs = run_kwargs
|
|
14
|
+
|
|
15
|
+
async def respond(self, text: str) -> AsyncIterator[AgentEvent]:
|
|
16
|
+
async with self.agent.run(text, **{**self.run_kwargs, "stream": True}) as stream:
|
|
17
|
+
async for update in stream:
|
|
18
|
+
if update.text:
|
|
19
|
+
yield AgentEvent.text(update.text)
|
|
20
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""OpenAI Agents SDK adapter; the SDK is an optional dependency."""
|
|
2
|
+
|
|
3
|
+
from contextlib import aclosing
|
|
4
|
+
from typing import Any, AsyncIterator
|
|
5
|
+
from yapwrap.core.base import BaseAgent
|
|
6
|
+
from yapwrap.core.events import AgentEvent
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OpenAIAgentsSDKAgent(BaseAgent):
|
|
10
|
+
"""Adapt an SDK Agent, with optional Runner and run options (e.g. session)."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, agent: Any, runner: Any = None, **run_kwargs: Any) -> None:
|
|
13
|
+
self.agent = agent
|
|
14
|
+
self.runner = runner
|
|
15
|
+
self.run_kwargs = run_kwargs
|
|
16
|
+
|
|
17
|
+
async def respond(self, text: str) -> AsyncIterator[AgentEvent]:
|
|
18
|
+
runner = self.runner
|
|
19
|
+
if runner is None:
|
|
20
|
+
from agents import Runner
|
|
21
|
+
runner = Runner
|
|
22
|
+
result = runner.run_streamed(self.agent, input=text, **self.run_kwargs)
|
|
23
|
+
finished = False
|
|
24
|
+
try:
|
|
25
|
+
async with aclosing(result.stream_events()) as stream:
|
|
26
|
+
async for event in stream:
|
|
27
|
+
if event.type == "raw_response_event" and event.data.type == "response.output_text.delta":
|
|
28
|
+
if event.data.delta:
|
|
29
|
+
yield AgentEvent.text(event.data.delta)
|
|
30
|
+
finished = True
|
|
31
|
+
finally:
|
|
32
|
+
if not finished:
|
|
33
|
+
result.cancel()
|
|
34
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Pydantic AI text agent adapter."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, AsyncIterator
|
|
4
|
+
from yapwrap.core.base import BaseAgent
|
|
5
|
+
from yapwrap.core.events import AgentEvent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PydanticAIAgent(BaseAgent):
|
|
9
|
+
"""Adapt a text-output Agent; pass dependencies/history as run options."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, agent: Any, **run_kwargs: Any) -> None:
|
|
12
|
+
self.agent = agent
|
|
13
|
+
self.run_kwargs = run_kwargs
|
|
14
|
+
|
|
15
|
+
async def respond(self, text: str) -> AsyncIterator[AgentEvent]:
|
|
16
|
+
if hasattr(self.agent, "run_stream"):
|
|
17
|
+
async with self.agent.run_stream(text, **self.run_kwargs) as result:
|
|
18
|
+
async for delta in result.stream_text(delta=True):
|
|
19
|
+
if delta:
|
|
20
|
+
yield AgentEvent.text(delta)
|
|
21
|
+
else:
|
|
22
|
+
result = await self.agent.run(text, **self.run_kwargs)
|
|
23
|
+
if not isinstance(result.output, str):
|
|
24
|
+
raise TypeError("PydanticAIAgent requires a text-output agent")
|
|
25
|
+
if result.output:
|
|
26
|
+
yield AgentEvent.text(result.output)
|
|
27
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Hugging Face smolagents adapter."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any, AsyncIterator
|
|
5
|
+
from yapwrap.core.base import BaseAgent
|
|
6
|
+
from yapwrap.core.events import AgentEvent
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SmolAgentsAgent(BaseAgent):
|
|
10
|
+
"""Speak final answers without reading intermediate reasoning/tool logs.
|
|
11
|
+
|
|
12
|
+
Synchronous agent work runs in a worker thread. A running thread cannot be
|
|
13
|
+
forcibly cancelled; the next turn waits for it to finish before reusing it.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, agent: Any) -> None:
|
|
17
|
+
self.agent = agent
|
|
18
|
+
self._pending: Any = None
|
|
19
|
+
|
|
20
|
+
async def respond(self, text: str) -> AsyncIterator[AgentEvent]:
|
|
21
|
+
if self._pending is not None:
|
|
22
|
+
try:
|
|
23
|
+
await asyncio.shield(self._pending)
|
|
24
|
+
finally:
|
|
25
|
+
if self._pending.done():
|
|
26
|
+
self._pending = None
|
|
27
|
+
self._pending = asyncio.create_task(asyncio.to_thread(self.agent.run, text))
|
|
28
|
+
# Retrieve exceptions even if the session ends during the worker.
|
|
29
|
+
self._pending.add_done_callback(lambda task: task.exception() if not task.cancelled() else None)
|
|
30
|
+
try:
|
|
31
|
+
result = await asyncio.shield(self._pending)
|
|
32
|
+
if result is not None:
|
|
33
|
+
yield AgentEvent.text(str(result))
|
|
34
|
+
finally:
|
|
35
|
+
if self._pending.done():
|
|
36
|
+
self._pending = None
|
|
37
|
+
|
yapwrap/core/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""YapWrap core architecture: base classes, events, audio processing, memory, and session pipeline."""
|
|
2
|
+
|
|
3
|
+
from yapwrap.core.base import BaseAgent, BaseSTT, BaseTTS, BaseVAD, BaseTransport
|
|
4
|
+
from yapwrap.core.protocols import Transport, VAD, STT, Agent, TTS
|
|
5
|
+
from yapwrap.core.events import AgentEvent, SessionEvent, TranscriptEvent
|
|
6
|
+
from yapwrap.core.memory import InMemoryHistory, ConversationTurn
|
|
7
|
+
from yapwrap.core.session import VoiceSession
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"BaseAgent",
|
|
11
|
+
"BaseSTT",
|
|
12
|
+
"BaseTTS",
|
|
13
|
+
"BaseVAD",
|
|
14
|
+
"BaseTransport",
|
|
15
|
+
"Transport",
|
|
16
|
+
"VAD",
|
|
17
|
+
"STT",
|
|
18
|
+
"Agent",
|
|
19
|
+
"TTS",
|
|
20
|
+
"AgentEvent",
|
|
21
|
+
"SessionEvent",
|
|
22
|
+
"TranscriptEvent",
|
|
23
|
+
"InMemoryHistory",
|
|
24
|
+
"ConversationTurn",
|
|
25
|
+
"VoiceSession",
|
|
26
|
+
]
|
|
27
|
+
|