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
37 changes: 33 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,12 @@ jobs:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-3.12-${{ hashFiles('pyproject.toml') }}
- run: pip install -e ".[dev]"
- run: mypy call_use/ --ignore-missing-imports
- run: mypy call_use/ --ignore-missing-imports --check-untyped-defs

test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13"]
steps:
Expand Down Expand Up @@ -66,13 +67,41 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install bandit safety
- run: pip install bandit pip-audit
- run: bandit -r call_use/ -c pyproject.toml -ll
- run: safety check --short-report
- run: pip-audit --strict

commit-lint:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install commitizen
- run: cz check --rev-range origin/${{ github.base_ref }}..HEAD

pr-size:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check PR size
run: |
LINES=$(git diff --stat origin/${{ github.base_ref }}...HEAD -- '*.py' | tail -1 | awk '{print $4+$6}')
echo "Changed lines: ${LINES:-0}"
if [ "${LINES:-0}" -gt 500 ]; then
echo "::warning::PR exceeds 500 line guideline (${LINES} lines changed). Consider splitting."
fi

build:
runs-on: ubuntu-latest
needs: [lint, typecheck, test]
needs: [lint, typecheck, test, security]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ format:
ruff format call_use/ tests/

typecheck:
mypy call_use/ --ignore-missing-imports
mypy call_use/ --ignore-missing-imports --check-untyped-defs

security:
bandit -r call_use/ -c pyproject.toml -ll
safety check --short-report || true
pip-audit --strict

build: clean
python3 -m build
Expand Down
16 changes: 9 additions & 7 deletions call_use/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,11 +255,13 @@ async def on_enter(self):

def _handle_data(dp):
task = asyncio.create_task(self._on_data_received(dp))
task.add_done_callback(
lambda t: (
t.exception() and logger.error("data handler error", exc_info=t.exception())
)
)

def _log_task_error(t: asyncio.Task) -> None:
exc = t.exception()
if exc:
logger.error("data handler error", exc_info=exc)

task.add_done_callback(_log_task_error)

self._room.on("data_received", _handle_data)
await self._set_state(CallStateEnum.connected)
Expand Down Expand Up @@ -639,7 +641,7 @@ def _on_conversation_item(ev):
if getattr(msg, "role", None) != "assistant":
return
text = msg.text_content if hasattr(msg, "text_content") else str(msg)
if text:
if text and self._evidence:
asyncio.create_task(self._evidence.emit_transcript("agent", text))

@session.on("function_tools_executed")
Expand All @@ -653,7 +655,7 @@ def _on_tools_executed(ev):
keys = args.get("keys", "")
else:
keys = ""
if keys:
if keys and self._evidence:
asyncio.create_task(self._evidence.emit_dtmf(keys))

# Initial greeting — called AFTER session.start(), NOT in on_enter()
Expand Down
2 changes: 1 addition & 1 deletion call_use/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ def setup():
click.echo(click.style(f" \u2713 {provider['name']}", fg="green"))
click.echo()

for key_def in provider["keys"]: # type: ignore[union-attr]
for key_def in provider["keys"]: # type: ignore[union-attr, attr-defined]
_prompt_key(key_def, values) # type: ignore[arg-type]

# --- STT key ---
Expand Down
1 change: 1 addition & 0 deletions call_use/sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ def _on_data(dp):

async def _handle_approval():
loop = asyncio.get_running_loop()
assert self._on_approval is not None
result = await loop.run_in_executor(None, self._on_approval, event.data)
await self._send_approval_response(
room_name,
Expand Down
41 changes: 41 additions & 0 deletions tests/test_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,47 @@ def capture_on(event_name, handler):
# Give the task time to run
await asyncio.sleep(0.1)

async def test_on_enter_data_handler_error_callback_calls_logger(self):
"""on_enter error callback logs when _on_data_received raises (line 262)."""
agent = _make_agent()
agent._current_state = CallStateEnum.connected
agent._room = MagicMock()
agent._lk_api = MagicMock()
agent._lk_api.room.update_room_metadata = AsyncMock()
agent._room.local_participant.identity = "agent-abc"
agent._room.name = "test-room"

# Make _on_data_received raise so the done-callback error branch fires
agent._on_data_received = AsyncMock(side_effect=RuntimeError("boom"))

registered_handler = None

def capture_on(event_name, handler):
nonlocal registered_handler
registered_handler = handler

agent._room.on = capture_on

await agent.on_enter()
assert registered_handler is not None

dp = MagicMock()

with pytest.MonkeyPatch.context() as mp:
import call_use.agent as agent_mod

mock_logger = MagicMock()
mp.setattr(agent_mod, "logger", mock_logger)

registered_handler(dp)
# Let the task and done-callback run
await asyncio.sleep(0.1)

mock_logger.error.assert_called_once()
args = mock_logger.error.call_args
assert args[0][0] == "data handler error"
assert isinstance(args[1]["exc_info"], RuntimeError)

async def test_on_enter_no_room_returns_early(self):
"""on_enter returns early if room is None."""
agent = _make_agent()
Expand Down
Loading