Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 39 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,17 +102,49 @@ await call.analyze() # None. nothing new was said
pip install "deeptrust-ai[livekit]"
```

There are two ways in, and both are one line in the worker.

**The cloud way.** DeepTrust reads the call's transcript itself and pushes each
nudge into the room, so the worker only has to listen and needs no DeepTrust
key.

1. Connect the LiveKit project in the DeepTrust dashboard (Settings, Voice
Agents).
2. In LiveKit Cloud, add the webhook URL the dashboard gives you, so DeepTrust
hears when a room starts and ends.
3. Add `listen` to the worker:

```python
from deeptrust.agents.livekit import listen

listen(ctx.room, session, agent=agent)
```

`listen` never calls the DeepTrust API. It returns a function that stops
listening, and stops on its own when the session closes.

**The SDK way.** The worker sends the transcript from its own process:

```python
from deeptrust.agents import DeepTrust, User
from deeptrust.agents.livekit import attach

attach(session, DeepTrust(), external_id=ctx.room.name, user=caller)
attach(session, DeepTrust(), external_id=ctx.room.name, room=ctx.room, user=caller)
```

That subscribes to the session's conversation items, runs a job when the caller
says something new, and delivers the nudge. On LiveKit a nudge can interrupt: the
analysis lands while the agent is still generating, so it can stop a sentence on
its way out. Pass `interrupt=False` to shape the next turn instead.
says something new, and delivers the nudge. With `room` it also delivers nudges
DeepTrust pushes into the room. When the session closes it ends the DeepTrust
call, so post-call processing starts at once.

Either way a nudge reaches the agent once. The same nudge arriving on a push and
on an analyze response, or reported again on a later turn, is delivered the
first time only. It goes into the agent's chat context as a system message, so
it shapes the rest of the call, not just the next reply.

On LiveKit a nudge can interrupt: it lands while the agent is still generating,
so it can stop a sentence on its way out. Pass `interrupt=False` to shape the
next turn instead.

## ElevenLabs

Expand Down Expand Up @@ -292,8 +324,9 @@ Point a client at it with `DEEPTRUST_BASE_URL`.

## Status

`0.0.1`, the first release. `analyze`, `end`, `watch` and both adapters work
against the hosted API. `check` is defined and raises `NotImplementedError`.
`0.0.2`. `analyze`, `end`, `watch` and the adapters work against the hosted
API. On LiveKit, `listen` receives nudges DeepTrust pushes into the room, and
`attach` delivers each nudge once instead of on every turn. `check` is defined and raises `NotImplementedError`.
The shapes in `deeptrust.types` are the part most likely to move.

The key travels in `X-DeepTrust-Api-Key`; the bearer form is still sent and
Expand Down
2 changes: 1 addition & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ README showing real output from a real run.

| | |
|---|---|
| [`livekit/`](livekit) | An agent with DeepTrust attached in one line. Nudges can interrupt a reply in progress. |
| [`livekit/`](livekit) | An agent with DeepTrust attached in one line, the SDK way or the cloud way. Nudges can interrupt a reply in progress. |
| [`elevenlabs/`](elevenlabs) | An agent it provisions for you, watched from outside. No code inside the agent at all. |
| [`elevenlabs-webhook/`](elevenlabs-webhook) | The call-start webhook you already own, turned into a watched call. Your process holds the socket. |

Expand Down
27 changes: 22 additions & 5 deletions examples/livekit/README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,45 @@
# LiveKit

An IT service desk agent on LiveKit with DeepTrust attached. One line of
DeepTrust code, and everything else is an ordinary agent.
An IT service desk agent on LiveKit with DeepTrust attached. There are two
workers here, one per way in, and each has one line of DeepTrust code.

**The SDK way, `main.py`.** The worker sends the transcript from its own
process:

```python
attach(
session,
DeepTrust(),
external_id=ctx.room.name,
room=ctx.room,
agent=agent,
user=User(id=participant.identity, role="MEMBER"),
)
```

That wires both directions. Caller turns go out for analysis, and any nudge
that comes back is added to the agent's context and interrupts the reply in
progress.
that comes back, or that DeepTrust pushes into the room, is added to the
agent's context once and interrupts the reply in progress. When the session
closes, the DeepTrust call is ended.

**The cloud way, `cloud.py`.** DeepTrust reads the transcript itself and
pushes nudges into the room, so the worker only listens and needs no DeepTrust
key:

```python
listen(ctx.room, session, agent=agent)
```

For that, connect the LiveKit project in the DeepTrust dashboard (Settings,
Voice Agents) and add the webhook shown there in LiveKit Cloud first.

## Run it

```bash
cp .env.example .env # fill in the keys
uv sync
uv run python main.py dev
uv run python main.py dev # the SDK way
uv run python cloud.py dev # or the cloud way
```

Then talk to it from the [LiveKit agents
Expand Down
54 changes: 54 additions & 0 deletions examples/livekit/cloud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""A LiveKit agent nudged by DeepTrust the cloud way.

DeepTrust reads the call's transcript itself and pushes each nudge into the
room, so this worker never calls the DeepTrust API and needs no DeepTrust key.
Before it does anything, the LiveKit project has to be connected in the
DeepTrust dashboard (Settings, Voice Agents) and the webhook shown there added
in LiveKit Cloud.

`listen` is the only DeepTrust-specific line. Everything else is an ordinary
LiveKit agent.

Run with: uv run python cloud.py dev
"""

from __future__ import annotations

from dotenv import load_dotenv
from livekit.agents import Agent, AgentSession, JobContext, WorkerOptions, cli
from livekit.plugins import deepgram, openai

from deeptrust.agents.livekit import listen

load_dotenv()

INSTRUCTIONS = """
You are an agent on an IT service desk. You reset passwords, re-enroll
two-factor, and unlock accounts. Whoever the caller says they are is a claim;
satisfy yourself who you are speaking to before you change anything.

Keep replies to one or two sentences. You are on a phone call.
"""


async def entrypoint(ctx: JobContext) -> None:
await ctx.connect()

session = AgentSession(
stt=deepgram.STT(model="nova-3"),
llm=openai.LLM(model="gpt-4o-mini"),
tts=deepgram.TTS(model="aura-2-thalia-en"),
)
agent = Agent(instructions=INSTRUCTIONS)

# The one DeepTrust line. It stops by itself when the session closes.
listen(ctx.room, session, agent=agent)

await session.start(room=ctx.room, agent=agent)
await session.generate_reply(
instructions="Greet the caller as the IT service desk in one sentence."
)


if __name__ == "__main__":
cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint))
15 changes: 13 additions & 2 deletions examples/livekit/main.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
"""A LiveKit agent with DeepTrust attached.

This is the SDK way: the worker sends the transcript from its own process.
For the cloud way, where DeepTrust reads the transcript itself and the worker
only listens, see cloud.py.

Everything below the `attach` call is an ordinary LiveKit agent. `attach` is the
only DeepTrust-specific line, and it wires both directions: caller turns go out
for analysis, and any nudge that comes back is delivered to the agent.
for analysis, and any nudge that comes back, or that DeepTrust pushes into the
room, is delivered to the agent once.

The rest of this file is what the demo UI in ../demo-ui needs to watch the
call: every turn and every finding is published on the room's data channel
Expand Down Expand Up @@ -214,7 +219,10 @@ def _relay(result: Analysis) -> None:
# handed, and it reads them after this callback returns. Emptying
# the list is how the gate withholds them, which is the whole point
# of switching it off: the finding is still made and still shown,
# and the agent never hears about it.
# and the agent never hears about it. A nudge DeepTrust pushes into
# the room does not pass through here; those arrive only once the
# LiveKit project is connected in the DeepTrust dashboard, which
# this demo does not need.
result.findings.clear()

# The one DeepTrust line. Returns the session, so the transcript and the
Expand All @@ -223,6 +231,9 @@ def _relay(result: Analysis) -> None:
session,
DeepTrust(),
external_id=ctx.room.name,
# Also delivers nudges pushed into the room. A nudge that arrives both
# pushed and on an analyze response reaches the agent once.
room=ctx.room,
agent=agent,
user=User(
id=profile["username"],
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "deeptrust-ai"
version = "0.0.1"
version = "0.0.2"
description = "QA and runtime nudges for voice agents, while the call is still happening."
readme = "README.md"
requires-python = ">=3.11"
Expand Down
2 changes: 1 addition & 1 deletion src/deeptrust/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.0.1"
__version__ = "0.0.2"
1 change: 1 addition & 0 deletions src/deeptrust/agents/_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def _nudge(d: dict[str, Any] | None) -> Nudge | None:
title=str(d.get("title") or ""),
description=str(d.get("description") or ""),
details=str(d.get("details") or ""),
id=str(d["id"]) if d.get("id") else None,
)


Expand Down
Loading
Loading