diff --git a/agent_telephony/twilio/livekit-trunk-setup/.env.example b/agent_telephony/twilio/livekit-trunk-setup/.env.example index 837ee4fc..f9537a54 100644 --- a/agent_telephony/twilio/livekit-trunk-setup/.env.example +++ b/agent_telephony/twilio/livekit-trunk-setup/.env.example @@ -1,4 +1,5 @@ TWILIO_ACCOUNT_SID= TWILIO_AUTH_TOKEN= TWILIO_PHONE_NUMBER="+000000000000" -LIVEKIT_SIP_URI="sip:XXXXX.sip.livekit.cloud;transport=tcp" \ No newline at end of file +LIVEKIT_SIP_URI="sip:XXXXX.sip.livekit.cloud;transport=tcp" +TRUNK_NAME="LiveKit Trunk" \ No newline at end of file diff --git a/agent_telephony/twilio/livekit-trunk-setup/inbound_trunk.json b/agent_telephony/twilio/livekit-trunk-setup/inbound_trunk.json new file mode 100644 index 00000000..115dc1ea --- /dev/null +++ b/agent_telephony/twilio/livekit-trunk-setup/inbound_trunk.json @@ -0,0 +1,8 @@ +{ + "trunk": { + "name": "Inbound LiveKit Trunk", + "numbers": [ + "+12096194281" + ] + } +} \ No newline at end of file diff --git a/agent_telephony/twilio/livekit-trunk-setup/twilio_trunk.py b/agent_telephony/twilio/livekit-trunk-setup/twilio_trunk.py index 7e026e21..aebea6f5 100644 --- a/agent_telephony/twilio/livekit-trunk-setup/twilio_trunk.py +++ b/agent_telephony/twilio/livekit-trunk-setup/twilio_trunk.py @@ -16,10 +16,10 @@ def get_env_var(var_name): return value -def create_livekit_trunk(client, sip_uri): +def create_livekit_trunk(client, sip_uri, trunk_name): domain_name = f"livekit-trunk-{os.urandom(4).hex()}.pstn.twilio.com" trunk = client.trunking.v1.trunks.create( - friendly_name="LiveKit Trunk", + friendly_name=trunk_name, domain_name=domain_name, ) trunk.origination_urls.create( @@ -27,14 +27,14 @@ def create_livekit_trunk(client, sip_uri): weight=1, priority=1, enabled=True, - friendly_name="LiveKit SIP URI", + friendly_name=f"{trunk_name} SIP URI", ) - logging.info("Created new LiveKit Trunk.") + logging.info(f"Created new {trunk_name} trunk.") return trunk -def create_inbound_trunk(phone_number): - trunk_data = {"trunk": {"name": "Inbound LiveKit Trunk", "numbers": [phone_number]}} +def create_inbound_trunk(phone_number, trunk_name): + trunk_data = {"trunk": {"name": f"Inbound {trunk_name}", "numbers": [phone_number]}} with open("inbound_trunk.json", "w") as f: json.dump(trunk_data, f, indent=4) @@ -58,9 +58,9 @@ def create_inbound_trunk(phone_number): return None -def create_dispatch_rule(trunk_sid): +def create_dispatch_rule(trunk_sid, trunk_name): dispatch_rule_data = { - "name": "Inbound Dispatch Rule", + "name": f"Inbound {trunk_name} Dispatch Rule", "trunk_ids": [trunk_sid], "rule": {"dispatchRuleIndividual": {"roomPrefix": "call-"}}, } @@ -89,23 +89,25 @@ def main(): auth_token = get_env_var("TWILIO_AUTH_TOKEN") phone_number = get_env_var("TWILIO_PHONE_NUMBER") sip_uri = get_env_var("LIVEKIT_SIP_URI") + trunk_name = get_env_var("TRUNK_NAME") client = Client(account_sid, auth_token) existing_trunks = client.trunking.v1.trunks.list() livekit_trunk = next( - (trunk for trunk in existing_trunks if trunk.friendly_name == "LiveKit Trunk"), + (trunk for trunk in existing_trunks if trunk.friendly_name == trunk_name), None, ) if not livekit_trunk: - livekit_trunk = create_livekit_trunk(client, sip_uri) + livekit_trunk = create_livekit_trunk(client, sip_uri, trunk_name) else: - logging.info("LiveKit Trunk already exists. Using the existing trunk.") + logging.info(f"{trunk_name} already exists. Using the existing trunk.") - inbound_trunk_sid = create_inbound_trunk(phone_number) + + inbound_trunk_sid = create_inbound_trunk(phone_number, trunk_name) if inbound_trunk_sid: - create_dispatch_rule(inbound_trunk_sid) + create_dispatch_rule(inbound_trunk_sid, trunk_name) if __name__ == "__main__": diff --git a/agent_telephony/twilio/livekit_pipeline/src/pipeline.py b/agent_telephony/twilio/livekit_pipeline/src/pipeline.py index acca2520..85c28181 100644 --- a/agent_telephony/twilio/livekit_pipeline/src/pipeline.py +++ b/agent_telephony/twilio/livekit_pipeline/src/pipeline.py @@ -124,7 +124,7 @@ def on_metrics_collected(agent_metrics: metrics.AgentMetrics) -> None: cli.run_app( WorkerOptions( entrypoint_fnc=entrypoint, - agent_name="AgentStream", + agent_name="AgentTwilio", prewarm_fnc=prewarm, ) ) diff --git a/agent_telephony/twilio/readme.md b/agent_telephony/twilio/readme.md index edf1cfce..12a30e39 100644 --- a/agent_telephony/twilio/readme.md +++ b/agent_telephony/twilio/readme.md @@ -7,7 +7,10 @@ Build an AI agent that do an outbound call with Twilio and can interact with in - Docker (for running Restack) - Python 3.10 or higher -- Vapi account (for outbound calls) +- Twilio account (for outbound calls) +- Livekit account (for WebRTC pipeline) +- Deepgram account (for transcription) +- ElevenLabs account (for TTS) ### Trunk setup for outbound calls with Twilio diff --git a/agent_voice/livekit/livekit_pipeline/src/pipeline.py b/agent_voice/livekit/livekit_pipeline/src/pipeline.py index a4f1df69..687c5f70 100644 --- a/agent_voice/livekit/livekit_pipeline/src/pipeline.py +++ b/agent_voice/livekit/livekit_pipeline/src/pipeline.py @@ -124,7 +124,7 @@ def on_metrics_collected(agent_metrics: metrics.AgentMetrics) -> None: cli.run_app( WorkerOptions( entrypoint_fnc=entrypoint, - agent_name="AgentStream", + agent_name="AgentVoice", prewarm_fnc=prewarm, ) ) diff --git a/community/docling/.gitignore b/community/docling/.gitignore new file mode 100644 index 00000000..5984d091 --- /dev/null +++ b/community/docling/.gitignore @@ -0,0 +1,2 @@ +poetry.lock +.env diff --git a/community/docling/Dockerfile b/community/docling/Dockerfile new file mode 100644 index 00000000..f17bca88 --- /dev/null +++ b/community/docling/Dockerfile @@ -0,0 +1,26 @@ +FROM python:3.12-bookworm + +WORKDIR /app + +RUN apt-get update && apt-get install -y \ + git build-essential gcc libc-dev \ + && apt-get clean + +RUN pip install --no-cache-dir --timeout=60 --retries=5 poetry + +COPY pyproject.toml ./ + +COPY . . + +# Configure poetry to not create virtual environment +RUN poetry config virtualenvs.create false + +ENV PIP_EXTRA_INDEX_URL=https://download.pytorch.org/whl/cpu + +# Install dependencies +RUN poetry install --no-interaction --no-ansi + +# Expose port 80 +EXPOSE 80 + +CMD poetry run python -m src.services \ No newline at end of file diff --git a/community/docling/pyproject.toml b/community/docling/pyproject.toml new file mode 100644 index 00000000..cd171388 --- /dev/null +++ b/community/docling/pyproject.toml @@ -0,0 +1,37 @@ +[tool.poetry] +name = "docling-test" +version = "0.1.0" +description = "" +authors = ["Your Name "] +readme = "readme.md" +packages = [ + { include = "src" } +] + +[[tool.poetry.source]] +name = "pytorch" +url = "https://download.pytorch.org/whl/cpu" +priority = "supplemental" + +[tool.poetry.dependencies] +python = "^3.12" +pydantic-settings = "^2.7.1" +aiohttp = "^3.11.11" +pydantic = "^2.10.6" +tavily-python = "^0.5.1" +openai = "^1.61.1" +mistune = "^3.1.1" +requests = "^2.32.3" +beautifulsoup4 = "^4.13.3" +gitpython = "^3.1.44" +validators = "^0.34.0" +restack-ai = "0.0.62" +docling = "^2.25.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.scripts] +services = "src.services:run_services" + diff --git a/community/docling/readme.md b/community/docling/readme.md new file mode 100644 index 00000000..9cb64c18 --- /dev/null +++ b/community/docling/readme.md @@ -0,0 +1,9 @@ + + + +poetry env use 3.12 + +poetry install + +poetry run services + diff --git a/community/docling/src/agents/agent.py b/community/docling/src/agents/agent.py new file mode 100644 index 00000000..8ac14d04 --- /dev/null +++ b/community/docling/src/agents/agent.py @@ -0,0 +1,27 @@ +from pydantic import BaseModel +from restack_ai.agent import ( + agent, + log, +) + +class EndEvent(BaseModel): + end: bool + +class AgentInput(BaseModel): + test_input: str | None = None + +@agent.defn() +class Agent: + def __init__(self) -> None: + self.end = False + + @agent.event + async def end(self, end: EndEvent) -> EndEvent: + log.info("Received end") + self.end = True + return end + + @agent.run + async def run(self, agent_input: AgentInput): + log.info("Received agent input", agent_input=agent_input) + await agent.condition(lambda: self.end) \ No newline at end of file diff --git a/community/docling/src/client.py b/community/docling/src/client.py new file mode 100644 index 00000000..885bf8ea --- /dev/null +++ b/community/docling/src/client.py @@ -0,0 +1,19 @@ +import os + +from dotenv import load_dotenv +from restack_ai import Restack +from restack_ai.restack import CloudConnectionOptions + +# Load environment variables from a .env file +load_dotenv() + + +engine_id = os.getenv("RESTACK_ENGINE_ID") +address = os.getenv("RESTACK_ENGINE_ADDRESS") +api_key = os.getenv("RESTACK_ENGINE_API_KEY") +api_address = os.getenv("RESTACK_ENGINE_API_ADDRESS") + +connection_options = CloudConnectionOptions( + engine_id=engine_id, address=address, api_key=api_key, api_address=api_address +) +client = Restack(connection_options) diff --git a/community/docling/src/functions/__init__.py b/community/docling/src/functions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/community/docling/src/functions/function.py b/community/docling/src/functions/function.py new file mode 100644 index 00000000..6e8a8bd9 --- /dev/null +++ b/community/docling/src/functions/function.py @@ -0,0 +1,10 @@ +from restack_ai.function import function, log + +@function.defn(name="welcome") +async def welcome(function_input: str) -> str: + try: + log.info("welcome function started", function_input=function_input) + return f"Hello, {function_input}!" + except Exception as e: + log.error("welcome function failed", error=e) + raise e diff --git a/community/docling/src/services.py b/community/docling/src/services.py new file mode 100644 index 00000000..f31ce05f --- /dev/null +++ b/community/docling/src/services.py @@ -0,0 +1,32 @@ +import asyncio + +from src.agents.agent import Agent +from src.client import client +from src.functions.function import ( + welcome, +) +from src.workflows.workflow import ( + Workflow, +) + +async def main(): + + await client.start_service( + agents=[ + Agent, + ], + workflows=[ + Workflow, + ], + functions=[ + welcome, + ] + ) + + +def run_services(): + asyncio.run(main()) + + +if __name__ == "__main__": + run_services() \ No newline at end of file diff --git a/community/docling/src/workflows/workflow.py b/community/docling/src/workflows/workflow.py new file mode 100644 index 00000000..4507f903 --- /dev/null +++ b/community/docling/src/workflows/workflow.py @@ -0,0 +1,21 @@ +from datetime import timedelta +from pydantic import BaseModel +from restack_ai.workflow import workflow, import_functions, log +with import_functions(): + from src.functions.function import welcome + + +class Input(BaseModel): + name: str = "world" + +class Output(BaseModel): + result: str + +@workflow.defn() +class Workflow: + @workflow.run + async def run(self, workflow_input: Input) -> Output: + log.info("ChildWorkflow started") + result = await workflow.step(function=welcome, function_input=workflow_input.name, start_to_close_timeout=timedelta(seconds=120)) + log.info("ChildWorkflow completed", result=result) + return Output(result=result)