feat: broaden tool_resources to app_resources
Broaden `tool_resources` to `app_resources` for easy access not just in
tool handlers but in other places like custom `FrameProcessor`s.
Involves 3 changes:
- A rename: `tool_resources` -> `app_resources`
- A new property on `PipelineTask`: `app_resources`
- A new property on `FrameProcessor`: `pipeline_task`
Usage in tool handler:
async def get_weather(params: FunctionCallParams):
resources = cast(MyAppResources, params.app_resources)
...
Usage in custom `FrameProcessor`:
class MyProcessor(FrameProcessor):
async def process_frame(self, frame, direction):
await super().process_frame(frame, direction)
if self.pipeline_task is not None:
resources = cast(MyAppResources, self.pipeline_task.app_resources)
...
The previous `tool_resources` aliases (on `PipelineTask`,
`FunctionCallParams`, and `FrameProcessorSetup`) keep working but are
deprecated as of 1.2.0 and emit `DeprecationWarning`s.
This commit is contained in:
@@ -4,23 +4,33 @@
|
||||
# SPDX-License-Identifier: BSD 2-Clause License
|
||||
#
|
||||
|
||||
"""Example demonstrating ``PipelineTask(tool_resources=...)``.
|
||||
"""Example demonstrating ``PipelineTask(app_resources=...)``.
|
||||
|
||||
``tool_resources`` is an application-defined bag of anything you want every
|
||||
tool handler in a session to share by reference: database handles, HTTP
|
||||
clients, feature flags, per-user state, observability clients, in-memory
|
||||
caches — whatever fits your app. Pipecat passes it through untouched as
|
||||
``FunctionCallParams.tool_resources``.
|
||||
``app_resources`` is an application-defined bag of anything your
|
||||
application code may want to share across a session: database handles,
|
||||
HTTP clients, feature flags, per-user state, observability clients,
|
||||
in-memory caches — whatever fits your app. Pipecat passes it through
|
||||
untouched and exposes it as ``task.app_resources``, so any code with a
|
||||
handle on the task can read or mutate it.
|
||||
|
||||
This example uses a small ``ToolCallLogger`` as a stand-in for that "shared
|
||||
thing". A real app might just as easily pass a Postgres pool, a Redis
|
||||
client, a Stripe SDK instance, or any combination thereof. The mechanics
|
||||
shown here — construct once, hand to the task, read it from each handler,
|
||||
inspect it after the session — are the same regardless of what you put in.
|
||||
Two of the convenience aliases exercised below:
|
||||
|
||||
We bundle resources in a typed ``SessionResources`` dataclass and cast back
|
||||
to it at the top of each handler. Pipecat doesn't care what type you pass
|
||||
(a plain dict works too), but a typed container gives you autocomplete and
|
||||
- Tool handlers read it from ``FunctionCallParams.app_resources``.
|
||||
- Custom ``FrameProcessor`` subclasses read it from
|
||||
``self.pipeline_task.app_resources``.
|
||||
|
||||
This example uses two small loggers as stand-ins for that "shared thing":
|
||||
``ToolCallLogger`` (written from tool handlers) and
|
||||
``TranscriptionLogger`` (written from a custom ``FrameProcessor`` that
|
||||
sits in the pipeline). A real app might just as easily pass a Postgres
|
||||
pool, a Redis client, a Stripe SDK instance, or any combination thereof.
|
||||
The mechanics shown here — construct once, hand to the task, read it
|
||||
from each site, inspect it after the session — are the same regardless
|
||||
of what you put in.
|
||||
|
||||
We bundle resources in a typed ``AppResources`` dataclass and cast back
|
||||
to it at each read site. Pipecat doesn't care what type you pass (a
|
||||
plain dict works too), but a typed container gives you autocomplete and
|
||||
refactor safety instead of dict-by-string-key lookups.
|
||||
"""
|
||||
|
||||
@@ -28,7 +38,7 @@ import json
|
||||
import os
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any, cast
|
||||
|
||||
from dotenv import load_dotenv
|
||||
@@ -37,7 +47,7 @@ from loguru import logger
|
||||
from pipecat.adapters.schemas.function_schema import FunctionSchema
|
||||
from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
||||
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
||||
from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame
|
||||
from pipecat.frames.frames import Frame, LLMRunFrame, TranscriptionFrame, TTSSpeakFrame
|
||||
from pipecat.pipeline.pipeline import Pipeline
|
||||
from pipecat.pipeline.runner import PipelineRunner
|
||||
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
||||
@@ -46,6 +56,7 @@ from pipecat.processors.aggregators.llm_response_universal import (
|
||||
LLMContextAggregatorPair,
|
||||
LLMUserAggregatorParams,
|
||||
)
|
||||
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||
from pipecat.runner.types import RunnerArguments
|
||||
from pipecat.runner.utils import create_transport
|
||||
from pipecat.services.cartesia.tts import CartesiaTTSService
|
||||
@@ -86,30 +97,80 @@ class ToolCallLogger:
|
||||
return json.dumps(self._calls, indent=2)
|
||||
|
||||
|
||||
class TranscriptionLogger:
|
||||
"""Records final user transcriptions — written from a custom FrameProcessor."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the logger with an empty list of recorded transcriptions."""
|
||||
self._entries: list[dict[str, Any]] = []
|
||||
|
||||
def log_transcription(self, text: str) -> None:
|
||||
"""Record a transcription.
|
||||
|
||||
Args:
|
||||
text: The transcribed user utterance.
|
||||
"""
|
||||
entry = {
|
||||
"timestamp": datetime.now(UTC).isoformat(),
|
||||
"text": text,
|
||||
}
|
||||
self._entries.append(entry)
|
||||
logger.info(f"[TranscriptionLogger] {text!r}")
|
||||
|
||||
def dump(self) -> str:
|
||||
"""Return all recorded transcriptions as a JSON string."""
|
||||
return json.dumps(self._entries, indent=2)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SessionResources:
|
||||
"""Typed container for everything the tool handlers in this session share.
|
||||
class AppResources:
|
||||
"""Typed container for everything the app shares across this session.
|
||||
|
||||
Add fields here as the app grows (e.g. ``db: AsyncConnection``,
|
||||
``http: httpx.AsyncClient``). Handlers ``cast()`` ``params.tool_resources``
|
||||
to this type to get autocomplete and refactor safety.
|
||||
``http: httpx.AsyncClient``). Read sites ``cast()`` to this type to
|
||||
get autocomplete and refactor safety:
|
||||
|
||||
- In tools: ``cast(AppResources, params.app_resources)``.
|
||||
- In custom processors: ``cast(AppResources, self.pipeline_task.app_resources)``.
|
||||
"""
|
||||
|
||||
tool_call_logger: ToolCallLogger
|
||||
transcription_logger: TranscriptionLogger
|
||||
|
||||
|
||||
async def fetch_weather_from_api(params: FunctionCallParams):
|
||||
resources = cast(SessionResources, params.tool_resources)
|
||||
resources = cast(AppResources, params.app_resources)
|
||||
resources.tool_call_logger.log_tool_call(params.function_name, params.arguments)
|
||||
await params.result_callback({"conditions": "nice", "temperature": "75"})
|
||||
|
||||
|
||||
async def fetch_restaurant_recommendation(params: FunctionCallParams):
|
||||
resources = cast(SessionResources, params.tool_resources)
|
||||
resources = cast(AppResources, params.app_resources)
|
||||
resources.tool_call_logger.log_tool_call(params.function_name, params.arguments)
|
||||
await params.result_callback({"name": "The Golden Dragon"})
|
||||
|
||||
|
||||
class TranscriptionLoggingProcessor(FrameProcessor):
|
||||
"""Logs each final user transcription into the shared app resources.
|
||||
|
||||
Demonstrates the second read site for ``app_resources``: any custom
|
||||
``FrameProcessor`` can reach the same bag every tool handler sees by
|
||||
going through ``self.pipeline_task.app_resources``. ``pipeline_task``
|
||||
is ``None`` until the task sets the processor up, so we guard against
|
||||
that case.
|
||||
"""
|
||||
|
||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||
"""Forward all frames; log final user transcriptions on the way through."""
|
||||
await super().process_frame(frame, direction)
|
||||
|
||||
if isinstance(frame, TranscriptionFrame) and self.pipeline_task is not None:
|
||||
resources = cast(AppResources, self.pipeline_task.app_resources)
|
||||
resources.transcription_logger.log_transcription(frame.text)
|
||||
|
||||
await self.push_frame(frame, direction)
|
||||
|
||||
|
||||
# We use lambdas to defer transport parameter creation until the transport
|
||||
# type is selected at runtime.
|
||||
transport_params = {
|
||||
@@ -203,6 +264,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
|
||||
[
|
||||
transport.input(),
|
||||
stt,
|
||||
TranscriptionLoggingProcessor(),
|
||||
user_aggregator,
|
||||
llm,
|
||||
tts,
|
||||
@@ -211,10 +273,14 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
|
||||
]
|
||||
)
|
||||
|
||||
# Keep a local handle so we can read collected state after the session
|
||||
# Keep local handles so we can read collected state after the session
|
||||
# ends; Pipecat never copies or clears the object.
|
||||
tool_call_logger = ToolCallLogger()
|
||||
resources = SessionResources(tool_call_logger=tool_call_logger)
|
||||
transcription_logger = TranscriptionLogger()
|
||||
resources = AppResources(
|
||||
tool_call_logger=tool_call_logger,
|
||||
transcription_logger=transcription_logger,
|
||||
)
|
||||
|
||||
task = PipelineTask(
|
||||
pipeline,
|
||||
@@ -223,7 +289,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
|
||||
enable_usage_metrics=True,
|
||||
),
|
||||
idle_timeout_secs=runner_args.pipeline_idle_timeout_secs,
|
||||
tool_resources=resources,
|
||||
app_resources=resources,
|
||||
)
|
||||
|
||||
@transport.event_handler("on_client_connected")
|
||||
@@ -246,6 +312,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
|
||||
|
||||
# The session has ended; read whatever state the handlers built up.
|
||||
logger.info(f"Tool calls logged during session:\n{tool_call_logger.dump()}")
|
||||
logger.info(f"Transcriptions logged during session:\n{transcription_logger.dump()}")
|
||||
|
||||
|
||||
async def bot(runner_args: RunnerArguments):
|
||||
Reference in New Issue
Block a user