@pipedog/core 0.0.0 → 0.1.0
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.
- package/LICENSE +25 -0
- package/NOTICE +9 -0
- package/dist/adapters/base_llm_adapter.d.ts +266 -0
- package/dist/adapters/base_llm_adapter.js +489 -0
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.js +8 -0
- package/dist/adapters/schemas/direct_function.d.ts +202 -0
- package/dist/adapters/schemas/direct_function.js +302 -0
- package/dist/adapters/schemas/function_schema.d.ts +67 -0
- package/dist/adapters/schemas/function_schema.js +72 -0
- package/dist/adapters/schemas/index.d.ts +2 -0
- package/dist/adapters/schemas/index.js +8 -0
- package/dist/adapters/schemas/tools_schema.d.ts +57 -0
- package/dist/adapters/schemas/tools_schema.js +79 -0
- package/dist/adapters/services/index.d.ts +2 -0
- package/dist/adapters/services/index.js +8 -0
- package/dist/adapters/services/open_ai_adapter.d.ts +147 -0
- package/dist/adapters/services/open_ai_adapter.js +250 -0
- package/dist/audio/dtmf/dtmf_data.d.ts +3 -0
- package/dist/audio/dtmf/dtmf_data.js +22 -0
- package/dist/audio/dtmf/index.d.ts +2 -0
- package/dist/audio/dtmf/index.js +8 -0
- package/dist/audio/dtmf/types.d.ts +31 -0
- package/dist/audio/dtmf/types.js +35 -0
- package/dist/audio/dtmf/utils.d.ts +13 -0
- package/dist/audio/dtmf/utils.js +62 -0
- package/dist/audio/filters/base_audio_filter.d.ts +51 -0
- package/dist/audio/filters/base_audio_filter.js +17 -0
- package/dist/audio/filters/index.d.ts +2 -0
- package/dist/audio/filters/index.js +8 -0
- package/dist/audio/index.d.ts +2 -0
- package/dist/audio/index.js +8 -0
- package/dist/audio/krisp_instance.d.ts +95 -0
- package/dist/audio/krisp_instance.js +229 -0
- package/dist/audio/mixers/base_audio_mixer.d.ts +56 -0
- package/dist/audio/mixers/base_audio_mixer.js +16 -0
- package/dist/audio/mixers/index.d.ts +2 -0
- package/dist/audio/mixers/index.js +8 -0
- package/dist/audio/resamplers/base_audio_resampler.d.ts +46 -0
- package/dist/audio/resamplers/base_audio_resampler.js +35 -0
- package/dist/audio/resamplers/index.d.ts +2 -0
- package/dist/audio/resamplers/index.js +8 -0
- package/dist/audio/resamplers/resampy_resampler.d.ts +10 -0
- package/dist/audio/resamplers/resampy_resampler.js +15 -0
- package/dist/audio/resamplers/soxr_resampler.d.ts +54 -0
- package/dist/audio/resamplers/soxr_resampler.js +69 -0
- package/dist/audio/resamplers/soxr_stream_resampler.d.ts +75 -0
- package/dist/audio/resamplers/soxr_stream_resampler.js +146 -0
- package/dist/audio/turn/base_turn_analyzer.d.ts +84 -0
- package/dist/audio/turn/base_turn_analyzer.js +61 -0
- package/dist/audio/turn/index.d.ts +2 -0
- package/dist/audio/turn/index.js +8 -0
- package/dist/audio/turn/smart_turn/_whisper_features.d.ts +30 -0
- package/dist/audio/turn/smart_turn/_whisper_features.js +225 -0
- package/dist/audio/turn/smart_turn/base_smart_turn.d.ts +94 -0
- package/dist/audio/turn/smart_turn/base_smart_turn.js +239 -0
- package/dist/audio/turn/smart_turn/local_smart_turn_v3.d.ts +76 -0
- package/dist/audio/turn/smart_turn/local_smart_turn_v3.js +143 -0
- package/dist/audio/utils.d.ts +224 -0
- package/dist/audio/utils.js +634 -0
- package/dist/audio/vad/data/index.d.ts +2 -0
- package/dist/audio/vad/data/index.js +8 -0
- package/dist/audio/vad/index.d.ts +2 -0
- package/dist/audio/vad/index.js +8 -0
- package/dist/audio/vad/vad_analyzer.d.ts +126 -0
- package/dist/audio/vad/vad_analyzer.js +211 -0
- package/dist/audio/vad/vad_controller.d.ts +143 -0
- package/dist/audio/vad/vad_controller.js +228 -0
- package/dist/audio/volume.d.ts +38 -0
- package/dist/audio/volume.js +75 -0
- package/dist/bus/adapters/base.d.ts +35 -0
- package/dist/bus/adapters/base.js +19 -0
- package/dist/bus/adapters/index.d.ts +10 -0
- package/dist/bus/adapters/index.js +15 -0
- package/dist/bus/adapters/llm_context_adapter.d.ts +36 -0
- package/dist/bus/adapters/llm_context_adapter.js +82 -0
- package/dist/bus/adapters/tools_schema_adapter.d.ts +25 -0
- package/dist/bus/adapters/tools_schema_adapter.js +70 -0
- package/dist/bus/bridge_processor.d.ts +111 -0
- package/dist/bus/bridge_processor.js +166 -0
- package/dist/bus/bus.d.ts +110 -0
- package/dist/bus/bus.js +200 -0
- package/dist/bus/index.d.ts +22 -0
- package/dist/bus/index.js +27 -0
- package/dist/bus/local/async_queue.d.ts +12 -0
- package/dist/bus/local/async_queue.js +21 -0
- package/dist/bus/local/index.d.ts +3 -0
- package/dist/bus/local/index.js +8 -0
- package/dist/bus/messages.d.ts +378 -0
- package/dist/bus/messages.js +357 -0
- package/dist/bus/network/index.d.ts +16 -0
- package/dist/bus/network/index.js +21 -0
- package/dist/bus/queue.d.ts +27 -0
- package/dist/bus/queue.js +51 -0
- package/dist/bus/serializers/base.d.ts +25 -0
- package/dist/bus/serializers/base.js +14 -0
- package/dist/bus/serializers/index.d.ts +9 -0
- package/dist/bus/serializers/index.js +14 -0
- package/dist/bus/serializers/json.d.ts +77 -0
- package/dist/bus/serializers/json.js +367 -0
- package/dist/bus/subscriber.d.ts +46 -0
- package/dist/bus/subscriber.js +27 -0
- package/dist/bus/ui/index.d.ts +3 -0
- package/dist/bus/ui/index.js +8 -0
- package/dist/bus/ui/messages.d.ts +186 -0
- package/dist/bus/ui/messages.js +171 -0
- package/dist/classifiers/base_classifier.d.ts +263 -0
- package/dist/classifiers/base_classifier.js +346 -0
- package/dist/classifiers/cloudflare/clef/classifier.d.ts +70 -0
- package/dist/classifiers/cloudflare/clef/classifier.js +204 -0
- package/dist/classifiers/cloudflare/clef/client.d.ts +104 -0
- package/dist/classifiers/cloudflare/clef/client.js +234 -0
- package/dist/classifiers/cloudflare/clef/index.d.ts +2 -0
- package/dist/classifiers/cloudflare/clef/index.js +8 -0
- package/dist/classifiers/cloudflare/index.d.ts +2 -0
- package/dist/classifiers/cloudflare/index.js +8 -0
- package/dist/classifiers/http_client.d.ts +70 -0
- package/dist/classifiers/http_client.js +161 -0
- package/dist/classifiers/index.d.ts +2 -0
- package/dist/classifiers/index.js +8 -0
- package/dist/classifiers/jev/classifier.d.ts +8 -0
- package/dist/classifiers/jev/classifier.js +13 -0
- package/dist/classifiers/jev/client.d.ts +8 -0
- package/dist/classifiers/jev/client.js +13 -0
- package/dist/classifiers/jev/index.d.ts +2 -0
- package/dist/classifiers/jev/index.js +8 -0
- package/dist/classifiers/llm/classifier.d.ts +72 -0
- package/dist/classifiers/llm/classifier.js +304 -0
- package/dist/classifiers/llm/index.d.ts +2 -0
- package/dist/classifiers/llm/index.js +8 -0
- package/dist/classifiers/openai/decisions/classifier.d.ts +77 -0
- package/dist/classifiers/openai/decisions/classifier.js +241 -0
- package/dist/classifiers/openai/decisions/client.d.ts +108 -0
- package/dist/classifiers/openai/decisions/client.js +234 -0
- package/dist/classifiers/openai/decisions/index.d.ts +2 -0
- package/dist/classifiers/openai/decisions/index.js +8 -0
- package/dist/classifiers/openai/index.d.ts +2 -0
- package/dist/classifiers/openai/index.js +8 -0
- package/dist/classifiers/typesafe/index.d.ts +2 -0
- package/dist/classifiers/typesafe/index.js +8 -0
- package/dist/classifiers/typesafe/jev/classifier.d.ts +70 -0
- package/dist/classifiers/typesafe/jev/classifier.js +220 -0
- package/dist/classifiers/typesafe/jev/client.d.ts +105 -0
- package/dist/classifiers/typesafe/jev/client.js +233 -0
- package/dist/classifiers/typesafe/jev/index.d.ts +2 -0
- package/dist/classifiers/typesafe/jev/index.js +8 -0
- package/dist/clocks/base_clock.d.ts +13 -0
- package/dist/clocks/base_clock.js +14 -0
- package/dist/clocks/index.d.ts +2 -0
- package/dist/clocks/index.js +8 -0
- package/dist/clocks/system_clock.d.ts +15 -0
- package/dist/clocks/system_clock.js +25 -0
- package/dist/frames/frames.d.ts +2533 -0
- package/dist/frames/frames.js +2485 -0
- package/dist/frames/index.d.ts +2 -0
- package/dist/frames/index.js +8 -0
- package/dist/frames/protobufs/frames_pb2.d.ts +229 -0
- package/dist/frames/protobufs/frames_pb2.js +466 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +56 -0
- package/dist/metrics/index.d.ts +2 -0
- package/dist/metrics/index.js +8 -0
- package/dist/metrics/metrics.d.ts +276 -0
- package/dist/metrics/metrics.js +254 -0
- package/dist/observers/base_observer.d.ts +173 -0
- package/dist/observers/base_observer.js +151 -0
- package/dist/observers/error_observer.d.ts +75 -0
- package/dist/observers/error_observer.js +87 -0
- package/dist/observers/function_call_observer.d.ts +157 -0
- package/dist/observers/function_call_observer.js +222 -0
- package/dist/observers/index.d.ts +2 -0
- package/dist/observers/index.js +8 -0
- package/dist/observers/loggers/debug_log_observer.d.ts +91 -0
- package/dist/observers/loggers/debug_log_observer.js +228 -0
- package/dist/observers/loggers/index.d.ts +2 -0
- package/dist/observers/loggers/index.js +8 -0
- package/dist/observers/loggers/llm_log_observer.d.ts +24 -0
- package/dist/observers/loggers/llm_log_observer.js +110 -0
- package/dist/observers/loggers/metrics_log_observer.d.ts +85 -0
- package/dist/observers/loggers/metrics_log_observer.js +186 -0
- package/dist/observers/loggers/transcription_log_observer.d.ts +23 -0
- package/dist/observers/loggers/transcription_log_observer.js +74 -0
- package/dist/observers/service_metrics_observer.d.ts +168 -0
- package/dist/observers/service_metrics_observer.js +236 -0
- package/dist/observers/speaking_observer.d.ts +104 -0
- package/dist/observers/speaking_observer.js +150 -0
- package/dist/observers/startup_timing_observer.d.ts +221 -0
- package/dist/observers/startup_timing_observer.js +365 -0
- package/dist/observers/turn_tracking_observer.d.ts +78 -0
- package/dist/observers/turn_tracking_observer.js +189 -0
- package/dist/observers/user_bot_latency_observer.d.ts +411 -0
- package/dist/observers/user_bot_latency_observer.js +974 -0
- package/dist/pipeline/base_pipeline.d.ts +49 -0
- package/dist/pipeline/base_pipeline.js +101 -0
- package/dist/pipeline/capabilities.d.ts +76 -0
- package/dist/pipeline/capabilities.js +97 -0
- package/dist/pipeline/index.d.ts +2 -0
- package/dist/pipeline/index.js +8 -0
- package/dist/pipeline/job_context.d.ts +317 -0
- package/dist/pipeline/job_context.js +450 -0
- package/dist/pipeline/job_decorator.d.ts +54 -0
- package/dist/pipeline/job_decorator.js +92 -0
- package/dist/pipeline/llm_switcher.d.ts +84 -0
- package/dist/pipeline/llm_switcher.js +107 -0
- package/dist/pipeline/llm_with_backend.d.ts +435 -0
- package/dist/pipeline/llm_with_backend.js +806 -0
- package/dist/pipeline/parallel_pipeline.d.ts +83 -0
- package/dist/pipeline/parallel_pipeline.js +200 -0
- package/dist/pipeline/pipeline.d.ts +126 -0
- package/dist/pipeline/pipeline.js +196 -0
- package/dist/pipeline/service_switcher.d.ts +287 -0
- package/dist/pipeline/service_switcher.js +509 -0
- package/dist/pipeline/sync_parallel_pipeline.d.ts +192 -0
- package/dist/pipeline/sync_parallel_pipeline.js +387 -0
- package/dist/pipeline/worker.d.ts +814 -0
- package/dist/pipeline/worker.js +1639 -0
- package/dist/pipeline/worker_observer.d.ts +131 -0
- package/dist/pipeline/worker_observer.js +274 -0
- package/dist/pipeline/worker_ready_decorator.d.ts +45 -0
- package/dist/pipeline/worker_ready_decorator.js +76 -0
- package/dist/processors/aggregators/async_tool_messages.d.ts +146 -0
- package/dist/processors/aggregators/async_tool_messages.js +291 -0
- package/dist/processors/aggregators/dtmf_aggregator.d.ts +61 -0
- package/dist/processors/aggregators/dtmf_aggregator.js +144 -0
- package/dist/processors/aggregators/gated.d.ts +43 -0
- package/dist/processors/aggregators/gated.js +86 -0
- package/dist/processors/aggregators/gated_llm_context.d.ts +42 -0
- package/dist/processors/aggregators/gated_llm_context.js +86 -0
- package/dist/processors/aggregators/index.d.ts +2 -0
- package/dist/processors/aggregators/index.js +8 -0
- package/dist/processors/aggregators/llm_context.d.ts +370 -0
- package/dist/processors/aggregators/llm_context.js +580 -0
- package/dist/processors/aggregators/llm_context_summarizer.d.ts +219 -0
- package/dist/processors/aggregators/llm_context_summarizer.js +447 -0
- package/dist/processors/aggregators/llm_response.d.ts +37 -0
- package/dist/processors/aggregators/llm_response.js +71 -0
- package/dist/processors/aggregators/llm_response_universal.d.ts +966 -0
- package/dist/processors/aggregators/llm_response_universal.js +2394 -0
- package/dist/processors/aggregators/llm_text_processor.d.ts +53 -0
- package/dist/processors/aggregators/llm_text_processor.js +102 -0
- package/dist/processors/aggregators/sentence.d.ts +39 -0
- package/dist/processors/aggregators/sentence.js +69 -0
- package/dist/processors/async_generator.d.ts +42 -0
- package/dist/processors/async_generator.js +76 -0
- package/dist/processors/audio/audio_buffer_processor.d.ts +241 -0
- package/dist/processors/audio/audio_buffer_processor.js +565 -0
- package/dist/processors/audio/index.d.ts +2 -0
- package/dist/processors/audio/index.js +8 -0
- package/dist/processors/audio/vad_processor.d.ts +67 -0
- package/dist/processors/audio/vad_processor.js +103 -0
- package/dist/processors/consumer_processor.d.ts +49 -0
- package/dist/processors/consumer_processor.js +85 -0
- package/dist/processors/filters/frame_filter.d.ts +39 -0
- package/dist/processors/filters/frame_filter.js +52 -0
- package/dist/processors/filters/function_filter.d.ts +59 -0
- package/dist/processors/filters/function_filter.js +78 -0
- package/dist/processors/filters/identity_filter.d.ts +33 -0
- package/dist/processors/filters/identity_filter.js +37 -0
- package/dist/processors/filters/index.d.ts +2 -0
- package/dist/processors/filters/index.js +8 -0
- package/dist/processors/filters/null_filter.d.ts +34 -0
- package/dist/processors/filters/null_filter.js +47 -0
- package/dist/processors/filters/wake_notifier_filter.d.ts +48 -0
- package/dist/processors/filters/wake_notifier_filter.js +49 -0
- package/dist/processors/frame_processor.d.ts +360 -0
- package/dist/processors/frame_processor.js +932 -0
- package/dist/processors/frameworks/index.d.ts +2 -0
- package/dist/processors/frameworks/index.js +8 -0
- package/dist/processors/frameworks/rtvi/frames.d.ts +241 -0
- package/dist/processors/frameworks/rtvi/frames.js +259 -0
- package/dist/processors/frameworks/rtvi/index.d.ts +6 -0
- package/dist/processors/frameworks/rtvi/index.js +11 -0
- package/dist/processors/frameworks/rtvi/models.d.ts +1741 -0
- package/dist/processors/frameworks/rtvi/models.js +1807 -0
- package/dist/processors/frameworks/rtvi/observer.d.ts +340 -0
- package/dist/processors/frameworks/rtvi/observer.js +1113 -0
- package/dist/processors/frameworks/rtvi/processor.d.ts +182 -0
- package/dist/processors/frameworks/rtvi/processor.js +676 -0
- package/dist/processors/idle_frame_processor.d.ts +44 -0
- package/dist/processors/idle_frame_processor.js +86 -0
- package/dist/processors/index.d.ts +2 -0
- package/dist/processors/index.js +8 -0
- package/dist/processors/logger.d.ts +42 -0
- package/dist/processors/logger.js +87 -0
- package/dist/processors/metrics/frame_processor_metrics.d.ts +172 -0
- package/dist/processors/metrics/frame_processor_metrics.js +372 -0
- package/dist/processors/metrics/index.d.ts +2 -0
- package/dist/processors/metrics/index.js +8 -0
- package/dist/processors/producer_processor.d.ts +68 -0
- package/dist/processors/producer_processor.js +78 -0
- package/dist/processors/text_transformer.d.ts +30 -0
- package/dist/processors/text_transformer.js +48 -0
- package/dist/registry/index.d.ts +3 -0
- package/dist/registry/index.js +8 -0
- package/dist/registry/registry.d.ts +69 -0
- package/dist/registry/registry.js +149 -0
- package/dist/registry/types.d.ts +47 -0
- package/dist/registry/types.js +39 -0
- package/dist/serializers/base_serializer.d.ts +94 -0
- package/dist/serializers/base_serializer.js +82 -0
- package/dist/serializers/index.d.ts +2 -0
- package/dist/serializers/index.js +8 -0
- package/dist/services/ai_service.d.ts +145 -0
- package/dist/services/ai_service.js +262 -0
- package/dist/services/anthropic/index.d.ts +2 -0
- package/dist/services/anthropic/index.js +7 -0
- package/dist/services/assemblyai/index.d.ts +2 -0
- package/dist/services/assemblyai/index.js +7 -0
- package/dist/services/asyncai/index.d.ts +2 -0
- package/dist/services/asyncai/index.js +7 -0
- package/dist/services/aws/index.d.ts +2 -0
- package/dist/services/aws/index.js +7 -0
- package/dist/services/azure/index.d.ts +2 -0
- package/dist/services/azure/index.js +7 -0
- package/dist/services/baseten/index.d.ts +2 -0
- package/dist/services/baseten/index.js +7 -0
- package/dist/services/bland/index.d.ts +2 -0
- package/dist/services/bland/index.js +7 -0
- package/dist/services/camb/index.d.ts +2 -0
- package/dist/services/camb/index.js +7 -0
- package/dist/services/cartesia/index.d.ts +2 -0
- package/dist/services/cartesia/index.js +7 -0
- package/dist/services/cerebras/index.d.ts +2 -0
- package/dist/services/cerebras/index.js +7 -0
- package/dist/services/crusoe/index.d.ts +2 -0
- package/dist/services/crusoe/index.js +7 -0
- package/dist/services/deepgram/index.d.ts +2 -0
- package/dist/services/deepgram/index.js +7 -0
- package/dist/services/deepseek/index.d.ts +2 -0
- package/dist/services/deepseek/index.js +7 -0
- package/dist/services/elevenlabs/index.d.ts +2 -0
- package/dist/services/elevenlabs/index.js +7 -0
- package/dist/services/fal/index.d.ts +2 -0
- package/dist/services/fal/index.js +7 -0
- package/dist/services/fireworks/index.d.ts +2 -0
- package/dist/services/fireworks/index.js +7 -0
- package/dist/services/fish/index.d.ts +2 -0
- package/dist/services/fish/index.js +7 -0
- package/dist/services/funasr/index.d.ts +3 -0
- package/dist/services/funasr/index.js +8 -0
- package/dist/services/gladia/index.d.ts +2 -0
- package/dist/services/gladia/index.js +7 -0
- package/dist/services/google/index.d.ts +2 -0
- package/dist/services/google/index.js +7 -0
- package/dist/services/gradium/index.d.ts +2 -0
- package/dist/services/gradium/index.js +7 -0
- package/dist/services/grok/index.d.ts +2 -0
- package/dist/services/grok/index.js +7 -0
- package/dist/services/groq/index.d.ts +2 -0
- package/dist/services/groq/index.js +7 -0
- package/dist/services/heygen/index.d.ts +2 -0
- package/dist/services/heygen/index.js +7 -0
- package/dist/services/hume/index.d.ts +2 -0
- package/dist/services/hume/index.js +7 -0
- package/dist/services/image_service.d.ts +52 -0
- package/dist/services/image_service.js +60 -0
- package/dist/services/inception/index.d.ts +2 -0
- package/dist/services/inception/index.js +7 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.js +8 -0
- package/dist/services/inworld/index.d.ts +2 -0
- package/dist/services/inworld/index.js +7 -0
- package/dist/services/keenable/index.d.ts +2 -0
- package/dist/services/keenable/index.js +7 -0
- package/dist/services/kokoro/index.d.ts +2 -0
- package/dist/services/kokoro/index.js +7 -0
- package/dist/services/llm_service.d.ts +1106 -0
- package/dist/services/llm_service.js +2231 -0
- package/dist/services/mem0/index.d.ts +2 -0
- package/dist/services/mem0/index.js +7 -0
- package/dist/services/meta/index.d.ts +2 -0
- package/dist/services/meta/index.js +7 -0
- package/dist/services/minimax/index.d.ts +2 -0
- package/dist/services/minimax/index.js +7 -0
- package/dist/services/mistral/index.d.ts +2 -0
- package/dist/services/mistral/index.js +7 -0
- package/dist/services/moondream/index.d.ts +2 -0
- package/dist/services/moondream/index.js +7 -0
- package/dist/services/moonshine/index.d.ts +2 -0
- package/dist/services/moonshine/index.js +7 -0
- package/dist/services/nebius/index.d.ts +2 -0
- package/dist/services/nebius/index.js +7 -0
- package/dist/services/neuphonic/index.d.ts +2 -0
- package/dist/services/neuphonic/index.js +7 -0
- package/dist/services/novita/index.d.ts +2 -0
- package/dist/services/novita/index.js +7 -0
- package/dist/services/nvidia/index.d.ts +2 -0
- package/dist/services/nvidia/index.js +11 -0
- package/dist/services/ollama/index.d.ts +2 -0
- package/dist/services/ollama/index.js +7 -0
- package/dist/services/openai/index.d.ts +2 -0
- package/dist/services/openai/index.js +7 -0
- package/dist/services/openclaw/index.d.ts +14 -0
- package/dist/services/openclaw/index.js +19 -0
- package/dist/services/openrouter/index.d.ts +2 -0
- package/dist/services/openrouter/index.js +7 -0
- package/dist/services/perplexity/index.d.ts +2 -0
- package/dist/services/perplexity/index.js +7 -0
- package/dist/services/piper/index.d.ts +2 -0
- package/dist/services/piper/index.js +7 -0
- package/dist/services/pocket_tts/index.d.ts +2 -0
- package/dist/services/pocket_tts/index.js +7 -0
- package/dist/services/qwen/index.d.ts +2 -0
- package/dist/services/qwen/index.js +7 -0
- package/dist/services/resembleai/index.d.ts +2 -0
- package/dist/services/resembleai/index.js +7 -0
- package/dist/services/rime/index.d.ts +2 -0
- package/dist/services/rime/index.js +7 -0
- package/dist/services/sambanova/index.d.ts +2 -0
- package/dist/services/sambanova/index.js +7 -0
- package/dist/services/sarvam/index.d.ts +2 -0
- package/dist/services/sarvam/index.js +7 -0
- package/dist/services/settings.d.ts +356 -0
- package/dist/services/settings.js +437 -0
- package/dist/services/simli/index.d.ts +2 -0
- package/dist/services/simli/index.js +7 -0
- package/dist/services/smallest/index.d.ts +2 -0
- package/dist/services/smallest/index.js +7 -0
- package/dist/services/soniox/index.d.ts +2 -0
- package/dist/services/soniox/index.js +7 -0
- package/dist/services/speechify/index.d.ts +2 -0
- package/dist/services/speechify/index.js +7 -0
- package/dist/services/speechmatics/index.d.ts +2 -0
- package/dist/services/speechmatics/index.js +7 -0
- package/dist/services/stt_latency.d.ts +62 -0
- package/dist/services/stt_latency.js +71 -0
- package/dist/services/stt_service.d.ts +601 -0
- package/dist/services/stt_service.js +1044 -0
- package/dist/services/tavus/index.d.ts +2 -0
- package/dist/services/tavus/index.js +7 -0
- package/dist/services/together/index.d.ts +2 -0
- package/dist/services/together/index.js +7 -0
- package/dist/services/tts_service.d.ts +909 -0
- package/dist/services/tts_service.js +2018 -0
- package/dist/services/ultravox/index.d.ts +2 -0
- package/dist/services/ultravox/index.js +9 -0
- package/dist/services/vision_service.d.ts +56 -0
- package/dist/services/vision_service.js +62 -0
- package/dist/services/websocket_service.d.ts +213 -0
- package/dist/services/websocket_service.js +654 -0
- package/dist/services/whisper/index.d.ts +2 -0
- package/dist/services/whisper/index.js +9 -0
- package/dist/services/xai/index.d.ts +2 -0
- package/dist/services/xai/index.js +9 -0
- package/dist/services/xtts/index.d.ts +2 -0
- package/dist/services/xtts/index.js +9 -0
- package/dist/tests/index.d.ts +2 -0
- package/dist/tests/index.js +8 -0
- package/dist/tests/utils.d.ts +123 -0
- package/dist/tests/utils.js +212 -0
- package/dist/transcriptions/index.d.ts +2 -0
- package/dist/transcriptions/index.js +8 -0
- package/dist/transcriptions/language.d.ts +376 -0
- package/dist/transcriptions/language.js +543 -0
- package/dist/transports/base_input.d.ts +149 -0
- package/dist/transports/base_input.js +338 -0
- package/dist/transports/base_output.d.ts +432 -0
- package/dist/transports/base_output.js +1231 -0
- package/dist/transports/base_transport.d.ts +223 -0
- package/dist/transports/base_transport.js +167 -0
- package/dist/transports/daily/index.d.ts +2 -0
- package/dist/transports/daily/index.js +9 -0
- package/dist/transports/heygen/index.d.ts +2 -0
- package/dist/transports/heygen/index.js +9 -0
- package/dist/transports/index.d.ts +2 -0
- package/dist/transports/index.js +8 -0
- package/dist/transports/lemonslice/index.d.ts +2 -0
- package/dist/transports/lemonslice/index.js +9 -0
- package/dist/transports/livekit/index.d.ts +2 -0
- package/dist/transports/livekit/index.js +9 -0
- package/dist/transports/local/index.d.ts +2 -0
- package/dist/transports/local/index.js +9 -0
- package/dist/transports/moq/index.d.ts +2 -0
- package/dist/transports/moq/index.js +9 -0
- package/dist/transports/smallwebrtc/index.d.ts +2 -0
- package/dist/transports/smallwebrtc/index.js +9 -0
- package/dist/transports/tavus/index.d.ts +2 -0
- package/dist/transports/tavus/index.js +9 -0
- package/dist/transports/video_in_sampler.d.ts +157 -0
- package/dist/transports/video_in_sampler.js +258 -0
- package/dist/transports/vonage/index.d.ts +2 -0
- package/dist/transports/vonage/index.js +9 -0
- package/dist/transports/websocket/index.d.ts +2 -0
- package/dist/transports/websocket/index.js +9 -0
- package/dist/transports/whatsapp/index.d.ts +2 -0
- package/dist/transports/whatsapp/index.js +9 -0
- package/dist/turns/eager_end_of_turn_mixin.d.ts +70 -0
- package/dist/turns/eager_end_of_turn_mixin.js +122 -0
- package/dist/turns/empty_user_turn.d.ts +55 -0
- package/dist/turns/empty_user_turn.js +53 -0
- package/dist/turns/index.d.ts +2 -0
- package/dist/turns/index.js +8 -0
- package/dist/turns/speculation_gate.d.ts +169 -0
- package/dist/turns/speculation_gate.js +294 -0
- package/dist/turns/types.d.ts +33 -0
- package/dist/turns/types.js +35 -0
- package/dist/turns/user_idle_controller.d.ts +94 -0
- package/dist/turns/user_idle_controller.js +187 -0
- package/dist/turns/user_mute/always_user_mute_strategy.d.ts +15 -0
- package/dist/turns/user_mute/always_user_mute_strategy.js +29 -0
- package/dist/turns/user_mute/base_user_mute_strategy.d.ts +39 -0
- package/dist/turns/user_mute/base_user_mute_strategy.js +45 -0
- package/dist/turns/user_mute/first_speech_user_mute_strategy.d.ts +28 -0
- package/dist/turns/user_mute/first_speech_user_mute_strategy.js +52 -0
- package/dist/turns/user_mute/function_call_user_mute_strategy.d.ts +25 -0
- package/dist/turns/user_mute/function_call_user_mute_strategy.js +50 -0
- package/dist/turns/user_mute/index.d.ts +6 -0
- package/dist/turns/user_mute/index.js +11 -0
- package/dist/turns/user_mute/mute_until_first_bot_complete_user_mute_strategy.d.ts +43 -0
- package/dist/turns/user_mute/mute_until_first_bot_complete_user_mute_strategy.js +76 -0
- package/dist/turns/user_start/base_user_turn_start_strategy.d.ts +135 -0
- package/dist/turns/user_start/base_user_turn_start_strategy.js +134 -0
- package/dist/turns/user_start/external_user_turn_start_strategy.d.ts +45 -0
- package/dist/turns/user_start/external_user_turn_start_strategy.js +64 -0
- package/dist/turns/user_start/index.d.ts +8 -0
- package/dist/turns/user_start/index.js +16 -0
- package/dist/turns/user_start/krisp_viva_ip_user_turn_start_strategy.d.ts +133 -0
- package/dist/turns/user_start/krisp_viva_ip_user_turn_start_strategy.js +243 -0
- package/dist/turns/user_start/min_words_user_turn_start_strategy.d.ts +60 -0
- package/dist/turns/user_start/min_words_user_turn_start_strategy.js +102 -0
- package/dist/turns/user_start/transcription_user_turn_start_strategy.d.ts +29 -0
- package/dist/turns/user_start/transcription_user_turn_start_strategy.js +43 -0
- package/dist/turns/user_start/vad_user_turn_start_strategy.d.ts +20 -0
- package/dist/turns/user_start/vad_user_turn_start_strategy.js +31 -0
- package/dist/turns/user_start/wake_phrase_user_turn_start_strategy.d.ts +141 -0
- package/dist/turns/user_start/wake_phrase_user_turn_start_strategy.js +238 -0
- package/dist/turns/user_stop/base_user_turn_stop_strategy.d.ts +174 -0
- package/dist/turns/user_stop/base_user_turn_stop_strategy.js +170 -0
- package/dist/turns/user_stop/deferred_user_turn_stop_strategy.d.ts +81 -0
- package/dist/turns/user_stop/deferred_user_turn_stop_strategy.js +107 -0
- package/dist/turns/user_stop/eager_match_policy.d.ts +36 -0
- package/dist/turns/user_stop/eager_match_policy.js +50 -0
- package/dist/turns/user_stop/eager_user_turn_stop_strategy.d.ts +84 -0
- package/dist/turns/user_stop/eager_user_turn_stop_strategy.js +154 -0
- package/dist/turns/user_stop/external_user_turn_completion_stop_strategy.d.ts +45 -0
- package/dist/turns/user_stop/external_user_turn_completion_stop_strategy.js +55 -0
- package/dist/turns/user_stop/external_user_turn_stop_strategy.d.ts +115 -0
- package/dist/turns/user_stop/external_user_turn_stop_strategy.js +205 -0
- package/dist/turns/user_stop/index.d.ts +10 -0
- package/dist/turns/user_stop/index.js +15 -0
- package/dist/turns/user_stop/llm_turn_completion_user_turn_stop_strategy.d.ts +44 -0
- package/dist/turns/user_stop/llm_turn_completion_user_turn_stop_strategy.js +59 -0
- package/dist/turns/user_stop/speech_timeout_user_turn_stop_strategy.d.ts +140 -0
- package/dist/turns/user_stop/speech_timeout_user_turn_stop_strategy.js +303 -0
- package/dist/turns/user_stop/turn_analyzer_user_turn_stop_strategy.d.ts +135 -0
- package/dist/turns/user_stop/turn_analyzer_user_turn_stop_strategy.js +332 -0
- package/dist/turns/user_turn_completion_mixin.d.ts +147 -0
- package/dist/turns/user_turn_completion_mixin.js +591 -0
- package/dist/turns/user_turn_controller.d.ts +134 -0
- package/dist/turns/user_turn_controller.js +335 -0
- package/dist/turns/user_turn_processor.d.ts +87 -0
- package/dist/turns/user_turn_processor.js +180 -0
- package/dist/turns/user_turn_strategies.d.ts +187 -0
- package/dist/turns/user_turn_strategies.js +222 -0
- package/dist/utils/async_tool_cancellation.d.ts +35 -0
- package/dist/utils/async_tool_cancellation.js +78 -0
- package/dist/utils/asyncio/index.d.ts +2 -0
- package/dist/utils/asyncio/index.js +7 -0
- package/dist/utils/asyncio/task_manager.d.ts +49 -0
- package/dist/utils/asyncio/task_manager.js +105 -0
- package/dist/utils/base_object.d.ts +76 -0
- package/dist/utils/base_object.js +154 -0
- package/dist/utils/context/aggregated_frame_sequencer.d.ts +491 -0
- package/dist/utils/context/aggregated_frame_sequencer.js +803 -0
- package/dist/utils/context/audio_context_manager.d.ts +131 -0
- package/dist/utils/context/audio_context_manager.js +233 -0
- package/dist/utils/context/index.d.ts +2 -0
- package/dist/utils/context/index.js +7 -0
- package/dist/utils/context/llm_context_summarization.d.ts +258 -0
- package/dist/utils/context/llm_context_summarization.js +511 -0
- package/dist/utils/context/text_segment_map.d.ts +611 -0
- package/dist/utils/context/text_segment_map.js +1132 -0
- package/dist/utils/context/word_completion_tracker.d.ts +273 -0
- package/dist/utils/context/word_completion_tracker.js +441 -0
- package/dist/utils/context/word_timestamp_tracker.d.ts +72 -0
- package/dist/utils/context/word_timestamp_tracker.js +98 -0
- package/dist/utils/deprecation.d.ts +28 -0
- package/dist/utils/deprecation.js +75 -0
- package/dist/utils/env.d.ts +38 -0
- package/dist/utils/env.js +62 -0
- package/dist/utils/errors.d.ts +75 -0
- package/dist/utils/errors.js +202 -0
- package/dist/utils/file_resolver.d.ts +126 -0
- package/dist/utils/file_resolver.js +314 -0
- package/dist/utils/file_storage.d.ts +99 -0
- package/dist/utils/file_storage.js +100 -0
- package/dist/utils/frame_queue.d.ts +36 -0
- package/dist/utils/frame_queue.js +66 -0
- package/dist/utils/http.d.ts +39 -0
- package/dist/utils/http.js +53 -0
- package/dist/utils/image.d.ts +15 -0
- package/dist/utils/image.js +30 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/log_config.d.ts +34 -0
- package/dist/utils/log_config.js +76 -0
- package/dist/utils/logger.d.ts +40 -0
- package/dist/utils/logger.js +91 -0
- package/dist/utils/mixin.d.ts +14 -0
- package/dist/utils/mixin.js +63 -0
- package/dist/utils/network.d.ts +73 -0
- package/dist/utils/network.js +90 -0
- package/dist/utils/security/allowed_origins.d.ts +16 -0
- package/dist/utils/security/allowed_origins.js +34 -0
- package/dist/utils/security/index.d.ts +2 -0
- package/dist/utils/security/index.js +7 -0
- package/dist/utils/security/redaction.d.ts +27 -0
- package/dist/utils/security/redaction.js +123 -0
- package/dist/utils/security/ssrf.d.ts +79 -0
- package/dist/utils/security/ssrf.js +327 -0
- package/dist/utils/shared.d.ts +35 -0
- package/dist/utils/shared.js +136 -0
- package/dist/utils/startup.d.ts +17 -0
- package/dist/utils/startup.js +130 -0
- package/dist/utils/string.d.ts +116 -0
- package/dist/utils/string.js +460 -0
- package/dist/utils/sync/base_notifier.d.ts +25 -0
- package/dist/utils/sync/base_notifier.js +16 -0
- package/dist/utils/sync/event_notifier.d.ts +38 -0
- package/dist/utils/sync/event_notifier.js +51 -0
- package/dist/utils/sync/index.d.ts +2 -0
- package/dist/utils/sync/index.js +7 -0
- package/dist/utils/text/alnum_utils.d.ts +109 -0
- package/dist/utils/text/alnum_utils.js +262 -0
- package/dist/utils/text/base_text_aggregator.d.ts +146 -0
- package/dist/utils/text/base_text_aggregator.js +102 -0
- package/dist/utils/text/base_text_filter.d.ts +54 -0
- package/dist/utils/text/base_text_filter.js +50 -0
- package/dist/utils/text/index.d.ts +2 -0
- package/dist/utils/text/index.js +7 -0
- package/dist/utils/text/markdown_text_filter.d.ts +147 -0
- package/dist/utils/text/markdown_text_filter.js +522 -0
- package/dist/utils/text/markup_utils.d.ts +54 -0
- package/dist/utils/text/markup_utils.js +156 -0
- package/dist/utils/text/pattern_pair_aggregator.d.ts +251 -0
- package/dist/utils/text/pattern_pair_aggregator.js +478 -0
- package/dist/utils/text/phonemes.d.ts +60 -0
- package/dist/utils/text/phonemes.js +172 -0
- package/dist/utils/text/simple_text_aggregator.d.ts +135 -0
- package/dist/utils/text/simple_text_aggregator.js +254 -0
- package/dist/utils/text/skip_tags_aggregator.d.ts +85 -0
- package/dist/utils/text/skip_tags_aggregator.js +147 -0
- package/dist/utils/text/transforms/pronunciations.d.ts +65 -0
- package/dist/utils/text/transforms/pronunciations.js +111 -0
- package/dist/utils/text/word_timestamp_utils.d.ts +26 -0
- package/dist/utils/text/word_timestamp_utils.js +52 -0
- package/dist/utils/time.d.ts +13 -0
- package/dist/utils/time.js +31 -0
- package/dist/utils/tracing/index.d.ts +3 -0
- package/dist/utils/tracing/index.js +8 -0
- package/dist/utils/tracing/service_attributes.d.ts +198 -0
- package/dist/utils/tracing/service_attributes.js +434 -0
- package/dist/utils/tracing/service_decorators.d.ts +214 -0
- package/dist/utils/tracing/service_decorators.js +1662 -0
- package/dist/utils/tracing/setup.d.ts +95 -0
- package/dist/utils/tracing/setup.js +110 -0
- package/dist/utils/tracing/tracing_context.d.ts +55 -0
- package/dist/utils/tracing/tracing_context.js +97 -0
- package/dist/utils/tracing/turn_trace_observer.d.ts +90 -0
- package/dist/utils/tracing/turn_trace_observer.js +219 -0
- package/dist/utils/types.d.ts +80 -0
- package/dist/utils/types.js +108 -0
- package/dist/utils/utils.d.ts +5 -0
- package/dist/utils/utils.js +22 -0
- package/dist/utils/yaml.d.ts +64 -0
- package/dist/utils/yaml.js +41 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.js +7 -0
- package/dist/workers/base_worker.d.ts +772 -0
- package/dist/workers/base_worker.js +1298 -0
- package/dist/workers/index.d.ts +2 -0
- package/dist/workers/index.js +8 -0
- package/dist/workers/llm/backend_llm_worker.d.ts +552 -0
- package/dist/workers/llm/backend_llm_worker.js +1055 -0
- package/dist/workers/llm/index.d.ts +6 -0
- package/dist/workers/llm/index.js +11 -0
- package/dist/workers/llm/llm_context_worker.d.ts +66 -0
- package/dist/workers/llm/llm_context_worker.js +72 -0
- package/dist/workers/llm/llm_worker.d.ts +180 -0
- package/dist/workers/llm/llm_worker.js +323 -0
- package/dist/workers/llm/tool_decorator.d.ts +68 -0
- package/dist/workers/llm/tool_decorator.js +60 -0
- package/dist/workers/runner.d.ts +268 -0
- package/dist/workers/runner.js +489 -0
- package/dist/workers/ui/index.d.ts +22 -0
- package/dist/workers/ui/index.js +27 -0
- package/dist/workers/ui/ui_event_decorator.d.ts +43 -0
- package/dist/workers/ui/ui_event_decorator.js +75 -0
- package/dist/workers/ui/ui_job_context.d.ts +12 -0
- package/dist/workers/ui/ui_job_context.js +17 -0
- package/dist/workers/ui/ui_prompts.d.ts +21 -0
- package/dist/workers/ui/ui_prompts.js +41 -0
- package/dist/workers/ui/ui_tools.d.ts +32 -0
- package/dist/workers/ui/ui_tools.js +112 -0
- package/dist/workers/ui/ui_worker.d.ts +571 -0
- package/dist/workers/ui/ui_worker.js +1156 -0
- package/package.json +46 -3
- package/README.md +0 -5
|
@@ -0,0 +1,2231 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026, Daily
|
|
2
|
+
// Copyright (c) 2026, pipedog contributors
|
|
3
|
+
// SPDX-License-Identifier: BSD-2-Clause
|
|
4
|
+
//
|
|
5
|
+
// Ported from pipecat: src/pipecat/services/llm_service.py
|
|
6
|
+
/**
|
|
7
|
+
* Base classes for Large Language Model services with function calling support.
|
|
8
|
+
*
|
|
9
|
+
* Porting notes:
|
|
10
|
+
* - Cancelling a function call cancels the task running its handler. In
|
|
11
|
+
* JavaScript cancellation is delivered at cancellation points (see
|
|
12
|
+
* `@pipedog/asyncio`), so a handler awaiting foreign work (`fetch`, an
|
|
13
|
+
* SDK) should pass `currentSignal()` to it, or wrap the await in
|
|
14
|
+
* `abortable()`, to stop as promptly as the Python handler would.
|
|
15
|
+
* - Deprecated upstream APIs are not ported: `FunctionCallParams.tool_resources`,
|
|
16
|
+
* `register_direct_function()`, `unregister_direct_function()`, the
|
|
17
|
+
* `enable_async_tool_cancellation` option, the warnings for application-set
|
|
18
|
+
* turn-completion settings, and the legacy `LLMUpdateSettingsFrame(settings={...})`
|
|
19
|
+
* dict path.
|
|
20
|
+
*/
|
|
21
|
+
import { CancelledError, Queue, rethrowIfCancelled, sleep, TimeoutError, wait, waitFor, } from "@pipedog/asyncio";
|
|
22
|
+
import { DirectFunctionWrapper } from "../adapters/schemas/direct_function.js";
|
|
23
|
+
import { ToolsSchema } from "../adapters/schemas/tools_schema.js";
|
|
24
|
+
import { OpenAILLMAdapter } from "../adapters/services/open_ai_adapter.js";
|
|
25
|
+
import { EagerEndOfTurnCancelFrame, FunctionCallCancelFrame, FunctionCallFromLLM, FunctionCallInProgressFrame, FunctionCallResultFrame, FunctionCallResultProperties, FunctionCallsStartedFrame, InterruptionFrame, LLMConfigureOutputFrame, LLMContextFrame, LLMContextSummaryRequestFrame, LLMContextSummaryResultFrame, LLMFullResponseEndFrame, LLMFullResponseStartFrame, LLMServiceMetadataFrame, LLMTextFrame, LLMUpdateSettingsFrame, } from "../frames/frames.js";
|
|
26
|
+
import { ASYNC_TOOL_INSTRUCTIONS } from "../processors/aggregators/async_tool_messages.js";
|
|
27
|
+
import { LLMContext } from "../processors/aggregators/llm_context.js";
|
|
28
|
+
import { FrameDirection } from "../processors/frame_processor.js";
|
|
29
|
+
import { SpeculationGate } from "../turns/speculation_gate.js";
|
|
30
|
+
import { UserTurnCompletionLLMServiceMixin } from "../turns/user_turn_completion_mixin.js";
|
|
31
|
+
import { ASYNC_TOOL_CANCELLATION_INSTRUCTIONS, buildCancelToolSchema, CANCEL_TOOL_PREFIX, cancelToolName, } from "../utils/async_tool_cancellation.js";
|
|
32
|
+
import { DEFAULT_SUMMARIZATION_TIMEOUT, LLMContextSummarizationUtil, } from "../utils/context/llm_context_summarization.js";
|
|
33
|
+
import { ErrorCategory } from "../utils/errors.js";
|
|
34
|
+
import { FileResolver } from "../utils/file_resolver.js";
|
|
35
|
+
import { _getLlmContextLogMode, _LLMContextLogMode } from "../utils/log_config.js";
|
|
36
|
+
import { logger } from "../utils/logger.js";
|
|
37
|
+
import { assertGiven, isGiven, NOT_GIVEN } from "../utils/types.js";
|
|
38
|
+
import { AIService } from "./ai_service.js";
|
|
39
|
+
import { LLMSettings } from "./settings.js";
|
|
40
|
+
import { ConnectionClosed, State, WebsocketService, } from "./websocket_service.js";
|
|
41
|
+
/**
|
|
42
|
+
* Raised by a method a service doesn't implement (Python's
|
|
43
|
+
* `NotImplementedError`), e.g. `LLMService.runInference()`.
|
|
44
|
+
*/
|
|
45
|
+
export class NotImplementedError extends Error {
|
|
46
|
+
name = "NotImplementedError";
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Parameters for a function call.
|
|
50
|
+
*
|
|
51
|
+
* - `functionName`: the name of the function being called.
|
|
52
|
+
* - `toolCallId`: a unique identifier for the function call.
|
|
53
|
+
* - `arguments`: the arguments for the function.
|
|
54
|
+
* - `llm`: the LLMService instance being used.
|
|
55
|
+
* - `pipelineWorker`: the worker running the pipeline this call was made from.
|
|
56
|
+
* Carries worker-scoped state, most notably `pipelineWorker.appResources`.
|
|
57
|
+
* - `context`: the LLM context.
|
|
58
|
+
* - `resultCallback`: callback to deliver the result of the function call.
|
|
59
|
+
* For async function calls (`cancelOnInterruption: false`), call it with
|
|
60
|
+
* `properties: new FunctionCallResultProperties({ isFinal: false })` to push
|
|
61
|
+
* intermediate updates before the final result.
|
|
62
|
+
* - `appResources`: the application-defined resources passed to
|
|
63
|
+
* `new PipelineWorker(..., { appResources })`. Same object — passed by
|
|
64
|
+
* reference, not a copy. Use it to share DB handles, clients, state, feature
|
|
65
|
+
* flags, etc. across all of a session's tool handlers.
|
|
66
|
+
* - `workerRunner`: the runner hosting `pipelineWorker`. Use it to reach
|
|
67
|
+
* another worker on the runner by name, e.g.
|
|
68
|
+
* `params.workerRunner.getWorker("ui-jobs")`. Always set for a call made
|
|
69
|
+
* from a running pipeline; null only when these params were built by hand.
|
|
70
|
+
*
|
|
71
|
+
* A handler runs in its own task and is cancelled on interruption, timeout or
|
|
72
|
+
* an LLM cancel request. Pass `currentSignal()` to `fetch` and other foreign
|
|
73
|
+
* awaits so they stop too.
|
|
74
|
+
*/
|
|
75
|
+
export class FunctionCallParams {
|
|
76
|
+
functionName;
|
|
77
|
+
toolCallId;
|
|
78
|
+
arguments;
|
|
79
|
+
llm;
|
|
80
|
+
pipelineWorker;
|
|
81
|
+
context;
|
|
82
|
+
resultCallback;
|
|
83
|
+
appResources;
|
|
84
|
+
workerRunner;
|
|
85
|
+
constructor(options) {
|
|
86
|
+
this.functionName = options.functionName;
|
|
87
|
+
this.toolCallId = options.toolCallId;
|
|
88
|
+
this.arguments = options.arguments;
|
|
89
|
+
this.llm = options.llm;
|
|
90
|
+
this.pipelineWorker = options.pipelineWorker;
|
|
91
|
+
this.context = options.context;
|
|
92
|
+
this.resultCallback = options.resultCallback;
|
|
93
|
+
this.appResources = options.appResources ?? null;
|
|
94
|
+
this.workerRunner = options.workerRunner ?? null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Internal record of a registered function-call handler.
|
|
99
|
+
*
|
|
100
|
+
* Created by the service when a function is registered — directly via
|
|
101
|
+
* `registerFunction`, or automatically from a direct function advertised in
|
|
102
|
+
* an `LLMContext` / `LLMSetToolsFrame`. Application code doesn't construct
|
|
103
|
+
* these.
|
|
104
|
+
*
|
|
105
|
+
* - `functionName`: the name of the function (null for catch-all handler).
|
|
106
|
+
* - `handler`: the handler for processing function call parameters.
|
|
107
|
+
* - `cancelOnInterruption`: whether to cancel the call on interruption. When
|
|
108
|
+
* false the call is treated as asynchronous: the LLM continues the
|
|
109
|
+
* conversation immediately without waiting for the result, and the result
|
|
110
|
+
* is injected later via a developer message.
|
|
111
|
+
* - `timeoutSecs`: optional per-tool timeout in seconds. Overrides the global
|
|
112
|
+
* `functionCallTimeoutSecs` for this specific function. A call that runs
|
|
113
|
+
* past it is cancelled.
|
|
114
|
+
* - `cancellableByLlm`: whether the LLM may cancel this call while it runs.
|
|
115
|
+
* Only meaningful on an async call (`cancelOnInterruption: false`), since a
|
|
116
|
+
* synchronous one holds the LLM until it returns.
|
|
117
|
+
* - `autoRegistered`: true only for a direct function that was auto-registered
|
|
118
|
+
* from an advertised tool set (listed in an `LLMContext` or
|
|
119
|
+
* `LLMSetToolsFrame`). False for every explicitly registered handler —
|
|
120
|
+
* direct or non-direct — and for the catch-all and built-in handlers.
|
|
121
|
+
*/
|
|
122
|
+
export class FunctionCallRegistryItem {
|
|
123
|
+
functionName;
|
|
124
|
+
handler;
|
|
125
|
+
cancelOnInterruption;
|
|
126
|
+
timeoutSecs;
|
|
127
|
+
cancellableByLlm;
|
|
128
|
+
autoRegistered;
|
|
129
|
+
constructor(options) {
|
|
130
|
+
this.functionName = options.functionName;
|
|
131
|
+
this.handler = options.handler;
|
|
132
|
+
this.cancelOnInterruption = options.cancelOnInterruption;
|
|
133
|
+
this.timeoutSecs = options.timeoutSecs ?? null;
|
|
134
|
+
this.cancellableByLlm = options.cancellableByLlm ?? false;
|
|
135
|
+
this.autoRegistered = options.autoRegistered ?? false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Internal function call entry for the function call runner.
|
|
140
|
+
*
|
|
141
|
+
* The runner executes function calls in order.
|
|
142
|
+
*
|
|
143
|
+
* - `registryItem`: the registry item containing handler information.
|
|
144
|
+
* - `functionName`: the name of the function.
|
|
145
|
+
* - `toolCallId`: a unique identifier for the function call.
|
|
146
|
+
* - `arguments`: the arguments for the function.
|
|
147
|
+
* - `context`: the LLM context.
|
|
148
|
+
* - `runLlm`: optional flag to control LLM execution after function call.
|
|
149
|
+
* - `groupId`: shared identifier for all function calls from the same LLM
|
|
150
|
+
* response batch. Used to trigger the LLM exactly once when the last call in
|
|
151
|
+
* the group completes.
|
|
152
|
+
* - `settled`: whether the call has already reached a terminal state — a final
|
|
153
|
+
* result, a timeout, or a cancellation. Results reported after that are
|
|
154
|
+
* rejected, since the rest of the pipeline has stopped tracking the call.
|
|
155
|
+
*/
|
|
156
|
+
export class FunctionCallRunnerItem {
|
|
157
|
+
registryItem;
|
|
158
|
+
functionName;
|
|
159
|
+
toolCallId;
|
|
160
|
+
arguments;
|
|
161
|
+
context;
|
|
162
|
+
runLlm;
|
|
163
|
+
groupId;
|
|
164
|
+
settled;
|
|
165
|
+
constructor(options) {
|
|
166
|
+
this.registryItem = options.registryItem;
|
|
167
|
+
this.functionName = options.functionName;
|
|
168
|
+
this.toolCallId = options.toolCallId;
|
|
169
|
+
this.arguments = options.arguments;
|
|
170
|
+
this.context = options.context;
|
|
171
|
+
this.runLlm = options.runLlm ?? null;
|
|
172
|
+
this.groupId = options.groupId ?? null;
|
|
173
|
+
this.settled = options.settled ?? false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Attributes `toolOptions()` sets on a handler (upstream `@tool_options`),
|
|
178
|
+
* with the option names the log lines use.
|
|
179
|
+
*/
|
|
180
|
+
const TOOL_OPTION_NAMES = {
|
|
181
|
+
_pipecatCancelOnInterruption: "cancel_on_interruption",
|
|
182
|
+
_pipecatTimeoutSecs: "timeout_secs",
|
|
183
|
+
_pipecatCancellableByLlm: "cancellable_by_llm",
|
|
184
|
+
};
|
|
185
|
+
/** Python's `repr()` closely enough for log lines. */
|
|
186
|
+
function repr(value) {
|
|
187
|
+
if (typeof value === "string") {
|
|
188
|
+
return value.includes("'") && !value.includes('"') ? `"${value}"` : `'${value}'`;
|
|
189
|
+
}
|
|
190
|
+
if (value === null || value === undefined)
|
|
191
|
+
return "None";
|
|
192
|
+
if (value === true)
|
|
193
|
+
return "True";
|
|
194
|
+
if (value === false)
|
|
195
|
+
return "False";
|
|
196
|
+
if (Array.isArray(value))
|
|
197
|
+
return `[${value.map(repr).join(", ")}]`;
|
|
198
|
+
if (typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
|
|
199
|
+
const entries = Object.entries(value);
|
|
200
|
+
return `{${entries.map(([k, v]) => `${repr(k)}: ${repr(v)}`).join(", ")}}`;
|
|
201
|
+
}
|
|
202
|
+
return String(value);
|
|
203
|
+
}
|
|
204
|
+
/** Python's `f"{e}"` for an exception. */
|
|
205
|
+
function errorText(e) {
|
|
206
|
+
return e instanceof Error ? e.message : String(e);
|
|
207
|
+
}
|
|
208
|
+
/** A string attribute set on a handler function, if any. */
|
|
209
|
+
function handlerAttr(handler, attr) {
|
|
210
|
+
if (handler === null || handler === undefined)
|
|
211
|
+
return null;
|
|
212
|
+
return handler[attr] ?? null;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Base class for all LLM services.
|
|
216
|
+
*
|
|
217
|
+
* Handles function calling registration and execution with support for both
|
|
218
|
+
* parallel and sequential execution modes. Provides event handlers for
|
|
219
|
+
* completion timeouts and function call lifecycle events.
|
|
220
|
+
*
|
|
221
|
+
* The service supports the following event handlers:
|
|
222
|
+
*
|
|
223
|
+
* - on_completion_timeout: called when an LLM completion timeout occurs
|
|
224
|
+
* - on_function_calls_started: called when function calls are received and
|
|
225
|
+
* execution is about to start. Built-in tools (e.g. a `cancel_<name>` tool)
|
|
226
|
+
* are excluded from this event.
|
|
227
|
+
* - on_function_calls_cancelled: called after one or more async tool calls are
|
|
228
|
+
* cancelled.
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* llm.eventHandler("on_completion_timeout", async (service) => {
|
|
232
|
+
* logger.warning("LLM completion timed out");
|
|
233
|
+
* });
|
|
234
|
+
* llm.eventHandler("on_function_calls_started", async (service, functionCalls: FunctionCallFromLLM[]) => {
|
|
235
|
+
* logger.info(`Starting ${functionCalls.length} function calls`);
|
|
236
|
+
* });
|
|
237
|
+
* llm.eventHandler("on_function_calls_cancelled", async (service, functionCalls: FunctionCallFromLLM[]) => {
|
|
238
|
+
* logger.info(`Cancelled ${functionCalls.length} function calls`);
|
|
239
|
+
* });
|
|
240
|
+
*
|
|
241
|
+
* @throws InvalidEnvVarValueError (from the constructor) if
|
|
242
|
+
* `PIPECAT_LOG_LLM_CONTEXT` is set to an unknown mode.
|
|
243
|
+
*/
|
|
244
|
+
export class LLMService extends UserTurnCompletionLLMServiceMixin(AIService) {
|
|
245
|
+
_adapter;
|
|
246
|
+
/**
|
|
247
|
+
* Subclasses should set this to a more specific adapter when necessary.
|
|
248
|
+
* `null` selects OpenAILLMAdapter, which aligns with most LLM
|
|
249
|
+
* implementations.
|
|
250
|
+
*/
|
|
251
|
+
static adapterClass = null;
|
|
252
|
+
/**
|
|
253
|
+
* Returned to the LLM as the tool result when an unavailable function is
|
|
254
|
+
* called. Deliberately neutral about future availability so the LLM can
|
|
255
|
+
* pick the function up again if it returns (e.g. via the
|
|
256
|
+
* `addToolChangeMessages` activation message, or silently on a later
|
|
257
|
+
* inference). `{function_name}` is substituted at runtime.
|
|
258
|
+
*/
|
|
259
|
+
static MISSING_FUNCTION_CALL_MESSAGE_TEMPLATE = "The function `{function_name}` is not currently available.";
|
|
260
|
+
/**
|
|
261
|
+
* Returned to the LLM as the tool result when a handler raises. It names
|
|
262
|
+
* the function as the thing that failed. The exception itself is
|
|
263
|
+
* deliberately left out.
|
|
264
|
+
*/
|
|
265
|
+
static FUNCTION_CALL_ERROR_MESSAGE_TEMPLATE = "The function `{function_name}` failed and returned no result.";
|
|
266
|
+
/**
|
|
267
|
+
* Whether the provider can enforce a response schema in `runInference()`.
|
|
268
|
+
*
|
|
269
|
+
* Services whose provider can set this to true. When only some of its
|
|
270
|
+
* models can, they also override `modelSupportsResponseSchema()`.
|
|
271
|
+
*/
|
|
272
|
+
static supportsResponseSchema = false;
|
|
273
|
+
_runInParallel;
|
|
274
|
+
_groupParallelTools;
|
|
275
|
+
_functionCallTimeoutSecs;
|
|
276
|
+
_fileResolver;
|
|
277
|
+
_reportsTtfat = null;
|
|
278
|
+
// Turn completion is owned by LLMTurnCompletionUserTurnStopStrategy, which
|
|
279
|
+
// enables it over an LLMUpdateSettingsFrame once the pipeline starts.
|
|
280
|
+
_filterIncompleteUserTurns = false;
|
|
281
|
+
_loggedThinkingDefaultModel = null;
|
|
282
|
+
// Holds a speculative response until its turn is confirmed. Frames are
|
|
283
|
+
// routed through it on the way out, in `pushFrame`.
|
|
284
|
+
_speculationGate;
|
|
285
|
+
// The per-tool cancel tools currently advertised, by name.
|
|
286
|
+
_cancelToolNames = new Set();
|
|
287
|
+
// Cancel-tool names that collide with a user's own tool, warned about once.
|
|
288
|
+
_warnedCancelCollisions = new Set();
|
|
289
|
+
// The user's base system instruction, without composed addons. Captured
|
|
290
|
+
// here and refreshed when the user changes `systemInstruction` at runtime;
|
|
291
|
+
// `_composeSystemInstruction` always rebuilds the effective instruction
|
|
292
|
+
// from this plus any appended / addon instructions.
|
|
293
|
+
_baseSystemInstruction;
|
|
294
|
+
_appendedSystemInstructions = [];
|
|
295
|
+
// The instruction as last composed, so a recomposition that changes nothing
|
|
296
|
+
// (every tool sync recomposes) is not logged again.
|
|
297
|
+
_composedSystemInstruction = null;
|
|
298
|
+
_functions = new Map();
|
|
299
|
+
// Cleanup callables carried by registered tool handlers, awaited at service
|
|
300
|
+
// teardown (see _recordToolCleanup).
|
|
301
|
+
_toolCleanups = [];
|
|
302
|
+
// Names we've already warned about for a redundant manual registration (an
|
|
303
|
+
// explicit registerFunction call for a tool whose advertised FunctionSchema
|
|
304
|
+
// already carries a handler), so the warning fires once rather than on
|
|
305
|
+
// every context frame.
|
|
306
|
+
_redundantRegistrationWarned = new Set();
|
|
307
|
+
// Names explicitly unregistered (via unregisterFunction) that
|
|
308
|
+
// auto-registration must not re-register while they're still advertised —
|
|
309
|
+
// otherwise a standalone unregister would be undone by the next context
|
|
310
|
+
// frame. Cleared when the name is registered again or stops being
|
|
311
|
+
// advertised (see _syncRegisteredToolHandlers).
|
|
312
|
+
_explicitlyUnregisteredFunctionNames = new Set();
|
|
313
|
+
_functionCallTasks = new Map();
|
|
314
|
+
_sequentialRunnerTask = null;
|
|
315
|
+
_sequentialRunnerQueue = new Queue();
|
|
316
|
+
_skipTts = null;
|
|
317
|
+
_summaryTask = null;
|
|
318
|
+
// Whether the one-time realtime-service "no turn frames" warning has fired
|
|
319
|
+
// (see _warnIfRealtimeServiceEmitsNoTurnFrames).
|
|
320
|
+
_warnedRealtimeServiceNoTurnFrames = false;
|
|
321
|
+
// Bound handlers, so they compare equal wherever they are stored (Python
|
|
322
|
+
// bound methods compare equal; JavaScript closures don't).
|
|
323
|
+
#functionCallTaskDone = (task) => this._functionCallTaskFinished(task);
|
|
324
|
+
#missingFunctionCallHandler = (params) => this._missingFunctionCallHandler(params);
|
|
325
|
+
#cancelToolHandler = (params) => this._cancelToolHandler(params);
|
|
326
|
+
constructor(options = {}) {
|
|
327
|
+
super({
|
|
328
|
+
...options,
|
|
329
|
+
// Here in case subclass doesn't implement more specific settings (which
|
|
330
|
+
// hopefully should be rare)
|
|
331
|
+
settings: options.settings ?? new LLMSettings(),
|
|
332
|
+
});
|
|
333
|
+
// Filled in here rather than in every service's default settings, since
|
|
334
|
+
// only the services with thinking defaults read it.
|
|
335
|
+
if (!isGiven(this._settings.useProviderThinkingDefault)) {
|
|
336
|
+
this._settings.useProviderThinkingDefault = false;
|
|
337
|
+
}
|
|
338
|
+
// Validated here so a bad value stops the bot at startup; otherwise it
|
|
339
|
+
// would surface on the first inference, and only with DEBUG enabled.
|
|
340
|
+
_getLlmContextLogMode();
|
|
341
|
+
this._runInParallel = options.runInParallel ?? true;
|
|
342
|
+
this._groupParallelTools = options.groupParallelTools ?? true;
|
|
343
|
+
this._functionCallTimeoutSecs = options.functionCallTimeoutSecs ?? null;
|
|
344
|
+
this._fileResolver = options.fileResolver ?? new FileResolver();
|
|
345
|
+
this._speculationGate = new SpeculationGate({ name: `${this}::SpeculationGate` });
|
|
346
|
+
const baseSi = this._settings.systemInstruction;
|
|
347
|
+
this._baseSystemInstruction = typeof baseSi === "string" ? baseSi : null;
|
|
348
|
+
const adapterClass = this.constructor._resolveAdapterClass();
|
|
349
|
+
this._adapter = new adapterClass();
|
|
350
|
+
// The adapter reads the resolver's caches during every conversion —
|
|
351
|
+
// invocation params and logging alike.
|
|
352
|
+
this._adapter.fileResolver = this._fileResolver;
|
|
353
|
+
this._registerEventHandler("on_function_calls_started");
|
|
354
|
+
this._registerEventHandler("on_function_calls_cancelled");
|
|
355
|
+
this._registerEventHandler("on_completion_timeout");
|
|
356
|
+
}
|
|
357
|
+
/** Return the adapter class to instantiate for this service. */
|
|
358
|
+
static _resolveAdapterClass() {
|
|
359
|
+
if (LLMService.adapterClass !== null) {
|
|
360
|
+
return LLMService.adapterClass;
|
|
361
|
+
}
|
|
362
|
+
return OpenAILLMAdapter;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Get the LLM adapter instance.
|
|
366
|
+
*
|
|
367
|
+
* @returns The adapter instance used for LLM communication.
|
|
368
|
+
*/
|
|
369
|
+
getLlmAdapter() {
|
|
370
|
+
return this._adapter;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Create an LLM-specific message (as opposed to a standard message) for use
|
|
374
|
+
* in an LLMContext.
|
|
375
|
+
*
|
|
376
|
+
* @param message The message content.
|
|
377
|
+
* @returns A LLMSpecificMessage instance.
|
|
378
|
+
*/
|
|
379
|
+
createLlmSpecificMessage(message) {
|
|
380
|
+
return this.getLlmAdapter().createLlmSpecificMessage(message);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Log that the service is generating a response from the given context.
|
|
384
|
+
*
|
|
385
|
+
* The context is included unless the LLM context log mode is `off`.
|
|
386
|
+
*
|
|
387
|
+
* @param context The context sent with the request.
|
|
388
|
+
*/
|
|
389
|
+
_logLlmResponse(context) {
|
|
390
|
+
this._logLlmEvent("Generating LLM response", context);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Log that a realtime service is seeding its server-side conversation.
|
|
394
|
+
*
|
|
395
|
+
* Realtime services upload the context once per session (and again after a
|
|
396
|
+
* reset), then send only new items. The context is included unless the LLM
|
|
397
|
+
* context log mode is `off`.
|
|
398
|
+
*
|
|
399
|
+
* @param context The context being uploaded.
|
|
400
|
+
*/
|
|
401
|
+
_logLlmConversationSetup(context) {
|
|
402
|
+
this._logLlmEvent("Setting up LLM conversation", context);
|
|
403
|
+
}
|
|
404
|
+
_logLlmEvent(event, context) {
|
|
405
|
+
// Built only if a sink accepts DEBUG.
|
|
406
|
+
if (!logger.isEnabledFor("DEBUG"))
|
|
407
|
+
return;
|
|
408
|
+
if (_getLlmContextLogMode() === _LLMContextLogMode.OFF) {
|
|
409
|
+
logger.debug(`${this}: ${event}`);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const messages = this.getLlmAdapter().getMessagesForLogging(context);
|
|
413
|
+
logger.debug(`${this}: ${event} ${JSON.stringify(messages)}`);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Run a one-shot, out-of-band (i.e. out-of-pipeline) inference with the
|
|
417
|
+
* given LLM context.
|
|
418
|
+
*
|
|
419
|
+
* Must be implemented by subclasses.
|
|
420
|
+
*
|
|
421
|
+
* @param context The LLM context containing conversation history.
|
|
422
|
+
* @param options.maxTokens Optional maximum number of tokens to generate. If
|
|
423
|
+
* provided, overrides the service's default max_tokens/max_completion_tokens
|
|
424
|
+
* setting.
|
|
425
|
+
* @param options.systemInstruction Optional system instruction to use for
|
|
426
|
+
* this inference. If provided, overrides any system instruction in the
|
|
427
|
+
* context.
|
|
428
|
+
* @param options.responseSchema Optional JSON schema the reply must follow.
|
|
429
|
+
* Services that can have the provider enforce it return JSON text matching
|
|
430
|
+
* the schema. When the service or its current model cannot enforce one
|
|
431
|
+
* (see `supportsResponseSchema`), the schema is ignored with a warning.
|
|
432
|
+
* The schema must satisfy the strictest provider in use: every object
|
|
433
|
+
* lists all its properties as required and sets `additionalProperties` to
|
|
434
|
+
* false.
|
|
435
|
+
* @returns The LLM's response as a string, or null if no response is generated.
|
|
436
|
+
*/
|
|
437
|
+
async runInference(_context, _options = {}) {
|
|
438
|
+
throw new NotImplementedError(`run_inference() not supported by ${this.constructor.name}`);
|
|
439
|
+
}
|
|
440
|
+
/** The schema to send, or null with a warning if it cannot be enforced. */
|
|
441
|
+
_checkResponseSchema(responseSchema) {
|
|
442
|
+
if (responseSchema === null || responseSchema === undefined) {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
if (!this.supportsResponseSchema) {
|
|
446
|
+
logger.warning(`${this}: response_schema is not supported and is ignored`);
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
const model = this._settings.model;
|
|
450
|
+
const cls = this.constructor;
|
|
451
|
+
if (!cls.modelSupportsResponseSchema(isGiven(model) && model ? model : "")) {
|
|
452
|
+
logger.warning(`${this}: response_schema is not supported by model ${model ?? "None"} and is ignored`);
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
return responseSchema;
|
|
456
|
+
}
|
|
457
|
+
/** Whether this service's provider can enforce a response schema. */
|
|
458
|
+
get supportsResponseSchema() {
|
|
459
|
+
return this.constructor.supportsResponseSchema;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Whether a model can enforce a response schema, on a provider that can.
|
|
463
|
+
*
|
|
464
|
+
* @param model The model name.
|
|
465
|
+
*/
|
|
466
|
+
static modelSupportsResponseSchema(_model) {
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Whether this service reports time-to-first-answer-token.
|
|
471
|
+
*
|
|
472
|
+
* Speech-to-speech services answer in audio, which has no answer token to
|
|
473
|
+
* measure to, so only text-answering services report it. Derived from
|
|
474
|
+
* `serviceMetadataFrame()` and cached, since it is read once per streamed
|
|
475
|
+
* token.
|
|
476
|
+
*/
|
|
477
|
+
get reportsTtfat() {
|
|
478
|
+
if (this._reportsTtfat === null) {
|
|
479
|
+
this._reportsTtfat = !this.serviceMetadataFrame().isRealtimeService;
|
|
480
|
+
}
|
|
481
|
+
return this._reportsTtfat;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Whether this service delivers a tool's intermediate results to the model.
|
|
485
|
+
*
|
|
486
|
+
* An intermediate result is one reported with
|
|
487
|
+
* `new FunctionCallResultProperties({ isFinal: false })` while the call keeps
|
|
488
|
+
* running. A text service passes them on as context messages. A
|
|
489
|
+
* speech-to-speech service has to map them onto whatever second channel its
|
|
490
|
+
* provider offers, and not all of them have one.
|
|
491
|
+
*/
|
|
492
|
+
get acceptsIntermediateFunctionCallResults() {
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Why this service can't take the given role in an `LLMWithBackend`, if it
|
|
497
|
+
* can't.
|
|
498
|
+
*
|
|
499
|
+
* The composite raises with what this returns, so the objection says what
|
|
500
|
+
* to do instead. Services that work in both roles, which is nearly all of
|
|
501
|
+
* them, say nothing.
|
|
502
|
+
*
|
|
503
|
+
* @param role The role the service is being asked to take.
|
|
504
|
+
* @returns The reason, or null when the service can take the role.
|
|
505
|
+
*/
|
|
506
|
+
llmWithBackendRoleObjection(_role) {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Send a model the thinking default its service lists, and log it once per
|
|
511
|
+
* model.
|
|
512
|
+
*
|
|
513
|
+
* Services that send each model its lowest thinking setting call this when
|
|
514
|
+
* the request configures no thinking of its own. With
|
|
515
|
+
* `useProviderThinkingDefault` set, the service sends no default. The first
|
|
516
|
+
* request for each model logs the setting sent, or that the model runs at
|
|
517
|
+
* the provider's default, warning when it has no default.
|
|
518
|
+
*
|
|
519
|
+
* @param params The request params, updated in place. Each field of
|
|
520
|
+
* `defaultParams` is merged into the dict already there, if any.
|
|
521
|
+
* @param model The model the request is for.
|
|
522
|
+
* @param defaultParams The request fields to send, or null if the service
|
|
523
|
+
* lists no default for the model.
|
|
524
|
+
* @param options.canThink Whether the model can think, which decides
|
|
525
|
+
* whether a model with no default is warned about.
|
|
526
|
+
* @param options.provider The provider's name, for the log lines.
|
|
527
|
+
* @param options.settingsFields The settings fields that configure thinking,
|
|
528
|
+
* as the log lines name them, such as "`thinking`".
|
|
529
|
+
*/
|
|
530
|
+
_applyThinkingDefault(params, model, defaultParams, options) {
|
|
531
|
+
const { canThink, provider, settingsFields } = options;
|
|
532
|
+
const providerDefault = assertGiven(this._settings.useProviderThinkingDefault);
|
|
533
|
+
if (defaultParams !== null && !providerDefault) {
|
|
534
|
+
for (const [field, value] of Object.entries(defaultParams)) {
|
|
535
|
+
params[field] = { ...(params[field] ?? {}), ...value };
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (model === this._loggedThinkingDefaultModel) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
this._loggedThinkingDefaultModel = model;
|
|
542
|
+
const settingsClass = `${this.constructor.name}.Settings`;
|
|
543
|
+
if (providerDefault) {
|
|
544
|
+
if (canThink) {
|
|
545
|
+
logger.debug(`${this}: use_provider_thinking_default is set, so ${model} runs at ${provider}'s ` +
|
|
546
|
+
`default thinking. To change it, set ${settingsFields} in ${settingsClass}.`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
else if (defaultParams !== null) {
|
|
550
|
+
const setting = Object.entries(defaultParams)
|
|
551
|
+
.map(([field, value]) => `${field}=${repr(value)}`)
|
|
552
|
+
.join(", ");
|
|
553
|
+
logger.info(`${this}: sending ${setting} to ${model}, its lowest thinking setting, for the ` +
|
|
554
|
+
"fastest responses. If your bot needs multistep reasoning, set " +
|
|
555
|
+
`${settingsFields} in ${settingsClass}.`);
|
|
556
|
+
}
|
|
557
|
+
else if (canThink) {
|
|
558
|
+
logger.warning(`${this}: Pipecat has no thinking default for ${model}, so it runs at ` +
|
|
559
|
+
`${provider}'s default, which can think before replying and slow responses. ` +
|
|
560
|
+
`For the fastest responses, set ${settingsFields} in ${settingsClass} to the ` +
|
|
561
|
+
`lowest setting ${model} supports.`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* The metadata frame this LLM service broadcasts at start.
|
|
566
|
+
*
|
|
567
|
+
* The base returns a plain (non-realtime) frame; realtime (speech-to-speech)
|
|
568
|
+
* subclasses override this to set `isRealtimeService: true` and, when their
|
|
569
|
+
* turns are provider-driven, recommend `ExternalUserTurnStrategies` via
|
|
570
|
+
* `userTurnStrategies`.
|
|
571
|
+
*
|
|
572
|
+
* Mostly here for conceptual consistency — today only realtime services
|
|
573
|
+
* need to override it — but it's a natural placeholder for future
|
|
574
|
+
* LLM-service metadata.
|
|
575
|
+
*/
|
|
576
|
+
serviceMetadataFrame() {
|
|
577
|
+
return new LLMServiceMetadataFrame({ serviceName: this.name });
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Warn (once) when a realtime service won't emit its own turn frames.
|
|
581
|
+
*
|
|
582
|
+
* Realtime services call this from `serviceMetadataFrame()` at the point
|
|
583
|
+
* they determine whether their turns are provider-driven. When they aren't,
|
|
584
|
+
* downstream processors that expect `UserStarted/StoppedSpeakingFrame`
|
|
585
|
+
* (e.g. RTVI) may need local VAD/turn detection.
|
|
586
|
+
*/
|
|
587
|
+
_warnIfRealtimeServiceEmitsNoTurnFrames(emitsTurnFrames) {
|
|
588
|
+
if (emitsTurnFrames || this._warnedRealtimeServiceNoTurnFrames) {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
this._warnedRealtimeServiceNoTurnFrames = true;
|
|
592
|
+
logger.warning(`${this} is not emitting turn frames ` +
|
|
593
|
+
"(UserStartedSpeakingFrame/UserStoppedSpeakingFrame) — this service " +
|
|
594
|
+
"either doesn't support them or isn't configured to emit them. A " +
|
|
595
|
+
"couple of things to keep in mind:\n" +
|
|
596
|
+
" - Other processors in the pipeline (e.g. RTVI) may expect " +
|
|
597
|
+
"turn frames. You can enable local VAD/turn detection by " +
|
|
598
|
+
"setting a vad_analyzer in LLMUserAggregatorParams.\n" +
|
|
599
|
+
" - Be aware that local turns may NOT perfectly align with " +
|
|
600
|
+
'the "ground truth" of server-decided turns, so they should ' +
|
|
601
|
+
"be thought of as APPROXIMATE (unless, of course, you've " +
|
|
602
|
+
"also configured local turn detection to *drive* the " +
|
|
603
|
+
"realtime service's turns, e.g. by setting " +
|
|
604
|
+
"vad=GeminiVADParams(disabled=True)).");
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Start the LLM service.
|
|
608
|
+
*
|
|
609
|
+
* @param frame The start frame.
|
|
610
|
+
*/
|
|
611
|
+
async start(frame) {
|
|
612
|
+
await super.start(frame);
|
|
613
|
+
if (!this._runInParallel) {
|
|
614
|
+
await this._createSequentialRunnerTask();
|
|
615
|
+
}
|
|
616
|
+
// A realtime service can run a tool it was configured with before the
|
|
617
|
+
// first context frame reaches it, so the handlers of its own tools are
|
|
618
|
+
// registered up front. Context frames re-sync them from then on.
|
|
619
|
+
this._syncRegisteredToolHandlers(null);
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Stop the LLM service.
|
|
623
|
+
*
|
|
624
|
+
* @param frame The end frame.
|
|
625
|
+
*/
|
|
626
|
+
async stop(frame) {
|
|
627
|
+
await super.stop(frame);
|
|
628
|
+
if (!this._runInParallel) {
|
|
629
|
+
await this._cancelSequentialRunnerTask();
|
|
630
|
+
}
|
|
631
|
+
await this._cancelSummaryTask();
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Cancel the LLM service.
|
|
635
|
+
*
|
|
636
|
+
* @param frame The cancel frame.
|
|
637
|
+
*/
|
|
638
|
+
async cancel(frame) {
|
|
639
|
+
await super.cancel(frame);
|
|
640
|
+
if (!this._runInParallel) {
|
|
641
|
+
await this._cancelSequentialRunnerTask();
|
|
642
|
+
}
|
|
643
|
+
await this._cancelSummaryTask();
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Release LLM service resources at teardown.
|
|
647
|
+
*
|
|
648
|
+
* Besides this service's own tasks, runs any cleanup callables carried by
|
|
649
|
+
* registered tool handlers (e.g. closing an MCP client's connection).
|
|
650
|
+
*/
|
|
651
|
+
async cleanup() {
|
|
652
|
+
await super.cleanup();
|
|
653
|
+
if (!this._runInParallel) {
|
|
654
|
+
await this._cancelSequentialRunnerTask();
|
|
655
|
+
}
|
|
656
|
+
await this._cancelSummaryTask();
|
|
657
|
+
await this._cancelAllFunctionCallTasks();
|
|
658
|
+
await this._runToolCleanups();
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Append durable text to the system instruction, preserving the user's
|
|
662
|
+
* prompt.
|
|
663
|
+
*
|
|
664
|
+
* The text is composed onto the end of the system instruction (joined with
|
|
665
|
+
* a blank line) and re-applied on every inference, so it survives
|
|
666
|
+
* context-message resets (e.g. `new LLMMessagesUpdateFrame({ messages: [] })`).
|
|
667
|
+
* Intended for framework components that own an LLM and need to add
|
|
668
|
+
* standard guidance to a user-provided prompt — for example, `UIWorker`
|
|
669
|
+
* appends the UI wire-format guide. Appended instructions compose after the
|
|
670
|
+
* user's base prompt and alongside the turn-completion and
|
|
671
|
+
* async-tool-cancellation instructions.
|
|
672
|
+
*
|
|
673
|
+
* @param instruction The instruction text to append.
|
|
674
|
+
*/
|
|
675
|
+
appendSystemInstruction(instruction) {
|
|
676
|
+
this._appendedSystemInstructions.push(instruction);
|
|
677
|
+
this._composeSystemInstruction();
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Whether any registered tool runs asynchronously.
|
|
681
|
+
*
|
|
682
|
+
* @returns True when a tool is registered with `cancelOnInterruption: false`,
|
|
683
|
+
* which is what makes the async-tool guidance worth composing in. The
|
|
684
|
+
* built-in cancellation tool is registered as a synchronous one, so it
|
|
685
|
+
* never counts on its own.
|
|
686
|
+
*/
|
|
687
|
+
_hasAsyncTools() {
|
|
688
|
+
for (const item of this._functions.values()) {
|
|
689
|
+
if (!item.cancelOnInterruption)
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Rebuild `systemInstruction` from the base prompt and all active addons.
|
|
696
|
+
*
|
|
697
|
+
* Joins the user's base system instruction (the single source of truth,
|
|
698
|
+
* captured at construction and refreshed on runtime `systemInstruction`
|
|
699
|
+
* updates) with any appended instructions (e.g. the `UIWorker` prompt
|
|
700
|
+
* guide), turn completion instructions (when enabled), and async tool
|
|
701
|
+
* cancellation instructions (when enabled). Safe to call repeatedly — it
|
|
702
|
+
* always rebuilds from the base, so it never compounds, and it logs the
|
|
703
|
+
* result only when it differs from the previous composition.
|
|
704
|
+
*/
|
|
705
|
+
_composeSystemInstruction() {
|
|
706
|
+
const base = this._baseSystemInstruction;
|
|
707
|
+
const parts = base ? [base] : [];
|
|
708
|
+
parts.push(...this._appendedSystemInstructions);
|
|
709
|
+
if (this._filterIncompleteUserTurns) {
|
|
710
|
+
parts.push(this._userTurnCompletionConfig.completionInstructions);
|
|
711
|
+
}
|
|
712
|
+
if (this._cancelToolNames.size > 0) {
|
|
713
|
+
parts.push(ASYNC_TOOL_CANCELLATION_INSTRUCTIONS);
|
|
714
|
+
}
|
|
715
|
+
if (this._hasAsyncTools()) {
|
|
716
|
+
parts.push(ASYNC_TOOL_INSTRUCTIONS);
|
|
717
|
+
}
|
|
718
|
+
const composed = parts.filter((p) => p).join("\n\n") || null;
|
|
719
|
+
this._settings.systemInstruction = composed;
|
|
720
|
+
if (composed !== this._composedSystemInstruction) {
|
|
721
|
+
this._composedSystemInstruction = composed;
|
|
722
|
+
logger.debug(`${this}: System instruction composed: ${composed}`);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Apply a settings delta, handling turn-completion fields.
|
|
727
|
+
*
|
|
728
|
+
* @param delta An LLM settings delta.
|
|
729
|
+
* @returns Dict mapping changed field names to their previous values.
|
|
730
|
+
*/
|
|
731
|
+
async _updateSettings(delta) {
|
|
732
|
+
const changed = await super._updateSettings(delta);
|
|
733
|
+
if ("filterIncompleteUserTurns" in changed) {
|
|
734
|
+
const value = this._settings.filterIncompleteUserTurns;
|
|
735
|
+
this._filterIncompleteUserTurns = isGiven(value) && Boolean(value);
|
|
736
|
+
logger.info(`${this}: Incomplete turn filtering ` +
|
|
737
|
+
`${this._filterIncompleteUserTurns ? "enabled" : "disabled"}`);
|
|
738
|
+
}
|
|
739
|
+
if ("systemInstruction" in changed) {
|
|
740
|
+
// The user replaced the base prompt; re-snapshot it so composition
|
|
741
|
+
// rebuilds the effective instruction from the new value.
|
|
742
|
+
const baseSi = this._settings.systemInstruction;
|
|
743
|
+
this._baseSystemInstruction = typeof baseSi === "string" ? baseSi : null;
|
|
744
|
+
}
|
|
745
|
+
if ("userTurnCompletionConfig" in changed && this._filterIncompleteUserTurns) {
|
|
746
|
+
this.setUserTurnCompletionConfig(assertGiven(this._settings.userTurnCompletionConfig));
|
|
747
|
+
}
|
|
748
|
+
// Any of these fields changes the composed instruction; rebuild it.
|
|
749
|
+
if ("filterIncompleteUserTurns" in changed ||
|
|
750
|
+
"systemInstruction" in changed ||
|
|
751
|
+
"userTurnCompletionConfig" in changed) {
|
|
752
|
+
this._composeSystemInstruction();
|
|
753
|
+
}
|
|
754
|
+
return changed;
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Process a frame.
|
|
758
|
+
*
|
|
759
|
+
* @param frame The frame to process.
|
|
760
|
+
* @param direction The direction of frame processing.
|
|
761
|
+
*/
|
|
762
|
+
async processFrame(frame, direction) {
|
|
763
|
+
await super.processFrame(frame, direction);
|
|
764
|
+
if (frame instanceof InterruptionFrame) {
|
|
765
|
+
await this._handleInterruptions(frame);
|
|
766
|
+
}
|
|
767
|
+
else if (frame instanceof EagerEndOfTurnCancelFrame) {
|
|
768
|
+
await this._handleEagerEndOfTurnCancel(frame);
|
|
769
|
+
}
|
|
770
|
+
else if (frame instanceof LLMConfigureOutputFrame) {
|
|
771
|
+
this._skipTts = frame.skipTts;
|
|
772
|
+
}
|
|
773
|
+
else if (frame instanceof LLMUpdateSettingsFrame) {
|
|
774
|
+
if (frame.service !== null && frame.service !== undefined && frame.service !== this) {
|
|
775
|
+
await this.pushFrame(frame, direction);
|
|
776
|
+
}
|
|
777
|
+
else if (frame.delta !== null && frame.delta !== undefined) {
|
|
778
|
+
await this._updateSettings(frame.delta);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
else if (frame instanceof LLMContextSummaryRequestFrame) {
|
|
782
|
+
await this._handleSummaryRequest(frame);
|
|
783
|
+
}
|
|
784
|
+
if (frame instanceof LLMContextFrame) {
|
|
785
|
+
// Runs before the subclass starts the completion, so the gate knows
|
|
786
|
+
// what this inference answers before any of its frames arrive.
|
|
787
|
+
this._speculationGate.beginSpeculation(frame.speculation);
|
|
788
|
+
// Sync the registered handlers with the tools advertised in the
|
|
789
|
+
// context: register any newly advertised handler, drop the ones we
|
|
790
|
+
// auto-registered that are no longer advertised. The context carries
|
|
791
|
+
// the current tool set on every inference, so this is the single place
|
|
792
|
+
// tool changes take effect for text LLMs.
|
|
793
|
+
//
|
|
794
|
+
// Realtime (speech-to-speech) services run continuously and don't get a
|
|
795
|
+
// fresh context frame per turn, so they additionally call
|
|
796
|
+
// _syncRegisteredToolHandlers on their own LLMSetToolsFrame handling.
|
|
797
|
+
this._syncRegisteredToolHandlers(frame.context.tools);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Pushes a frame.
|
|
802
|
+
*
|
|
803
|
+
* @param frame The frame to push.
|
|
804
|
+
* @param direction The direction of frame pushing.
|
|
805
|
+
*/
|
|
806
|
+
async pushFrame(frame, direction = FrameDirection.DOWNSTREAM) {
|
|
807
|
+
if (frame instanceof LLMTextFrame ||
|
|
808
|
+
frame instanceof LLMFullResponseStartFrame ||
|
|
809
|
+
frame instanceof LLMFullResponseEndFrame) {
|
|
810
|
+
if (this._skipTts !== null) {
|
|
811
|
+
frame.skipTts = this._skipTts;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
// The gate decides synchronously, so its verdict can't be torn by another
|
|
815
|
+
// task pushing at the same time. Everything it hands back is pushed past
|
|
816
|
+
// it — routing that back through here would re-gate it.
|
|
817
|
+
const emitted = this._speculationGate.process(frame, direction);
|
|
818
|
+
for (const [gatedFrame, gatedDirection] of emitted) {
|
|
819
|
+
await super.pushFrame(gatedFrame, gatedDirection);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Push LLM text, using turn completion detection if enabled.
|
|
824
|
+
*
|
|
825
|
+
* This helper method simplifies text pushing in LLM implementations by
|
|
826
|
+
* handling the conditional logic for turn completion internally.
|
|
827
|
+
*
|
|
828
|
+
* @param text The text content from the LLM to push.
|
|
829
|
+
*/
|
|
830
|
+
async _pushLlmText(text) {
|
|
831
|
+
// Measured before turn-completion filtering, which can hold text back or
|
|
832
|
+
// drop it entirely — neither says anything about how fast the model
|
|
833
|
+
// answered.
|
|
834
|
+
if (this.reportsTtfat) {
|
|
835
|
+
await this.stopTtfatMetrics();
|
|
836
|
+
}
|
|
837
|
+
if (this._filterIncompleteUserTurns) {
|
|
838
|
+
await this._pushTurnText(text);
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
await this.pushFrame(new LLMTextFrame({ text }));
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
async _handleInterruptions(_frame) {
|
|
845
|
+
for (const [functionName, entry] of [...this._functions.entries()]) {
|
|
846
|
+
if (entry.cancelOnInterruption) {
|
|
847
|
+
await this._cancelFunctionCall(functionName);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Stop generating a response whose user turn turned out to be unfinished.
|
|
853
|
+
*
|
|
854
|
+
* The tokens are wasted either way; stopping keeps us from paying for the
|
|
855
|
+
* rest of them. Unlike an interruption this leaves the turn open — the bot
|
|
856
|
+
* never spoke, and the user is still mid-turn.
|
|
857
|
+
*/
|
|
858
|
+
async _handleEagerEndOfTurnCancel(_frame) {
|
|
859
|
+
// Runs before the frame reaches the gate, which is what clears the
|
|
860
|
+
// speculation, so this still sees the one being withdrawn.
|
|
861
|
+
if (!this._speculationGate.isSpeculating) {
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
logger.debug(`${this}: eager end of turn withdrawn, stopping the speculative inference`);
|
|
865
|
+
await this._startInterruption();
|
|
866
|
+
await this.stopAllMetrics();
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Handle context summarization request from aggregator.
|
|
870
|
+
*
|
|
871
|
+
* Processes a summarization request by generating a compressed summary of
|
|
872
|
+
* conversation history. Uses the adapter to format the summary according
|
|
873
|
+
* to the provider's requirements. Broadcasts the result back to the
|
|
874
|
+
* aggregator for context reconstruction.
|
|
875
|
+
*
|
|
876
|
+
* @param frame The summary request frame containing context and parameters.
|
|
877
|
+
*/
|
|
878
|
+
async _handleSummaryRequest(frame) {
|
|
879
|
+
logger.debug(`${this}: Processing summarization request ${frame.requestId}`);
|
|
880
|
+
// Create a background task to generate the summary without blocking
|
|
881
|
+
this._summaryTask = this.createTask(() => this._generateSummaryTask(frame), "_generate_summary_task");
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Background task to generate summary without blocking the pipeline.
|
|
885
|
+
*
|
|
886
|
+
* @param frame The summary request frame containing context and parameters.
|
|
887
|
+
*/
|
|
888
|
+
async _generateSummaryTask(frame) {
|
|
889
|
+
let summary = "";
|
|
890
|
+
let lastIndex = -1;
|
|
891
|
+
let error = null;
|
|
892
|
+
// Python `or`: a 0 timeout also falls back to the default.
|
|
893
|
+
const timeout = frame.summarizationTimeout || DEFAULT_SUMMARIZATION_TIMEOUT;
|
|
894
|
+
try {
|
|
895
|
+
[summary, lastIndex] = await waitFor(() => this._generateSummary(frame), timeout);
|
|
896
|
+
}
|
|
897
|
+
catch (e) {
|
|
898
|
+
rethrowIfCancelled(e);
|
|
899
|
+
if (e instanceof TimeoutError) {
|
|
900
|
+
await this.pushError(`Context summarization timed out after ${timeout}s`);
|
|
901
|
+
}
|
|
902
|
+
else {
|
|
903
|
+
error = `Error generating context summary: ${errorText(e)}`;
|
|
904
|
+
await this.pushError(error, { exception: e });
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
await this.broadcastFrame(LLMContextSummaryResultFrame, {
|
|
908
|
+
requestId: frame.requestId,
|
|
909
|
+
summary,
|
|
910
|
+
lastSummarizedIndex: lastIndex,
|
|
911
|
+
error,
|
|
912
|
+
});
|
|
913
|
+
this._summaryTask = null;
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Generate a compressed summary of conversation context.
|
|
917
|
+
*
|
|
918
|
+
* Uses the message selection logic to identify which messages to
|
|
919
|
+
* summarize, formats them as a transcript, and invokes the LLM to generate
|
|
920
|
+
* a concise summary. The summary is formatted according to the LLM
|
|
921
|
+
* provider's requirements using the adapter.
|
|
922
|
+
*
|
|
923
|
+
* Requires the service to implement `runInference()` for one-shot LLM calls.
|
|
924
|
+
*
|
|
925
|
+
* @param frame The summary request frame containing context and configuration.
|
|
926
|
+
* @returns Tuple of (formatted summary message, lastSummarizedIndex).
|
|
927
|
+
* @throws Error if there are no messages to summarize, the service doesn't
|
|
928
|
+
* support runInference(), or the LLM returns an empty summary.
|
|
929
|
+
*/
|
|
930
|
+
async _generateSummary(frame) {
|
|
931
|
+
// Get messages to summarize using utility method
|
|
932
|
+
const result = LLMContextSummarizationUtil.getMessagesToSummarize(frame.context, frame.minMessagesToKeep);
|
|
933
|
+
if (result.messages.length === 0) {
|
|
934
|
+
logger.debug(`${this}: No messages to summarize`);
|
|
935
|
+
throw new Error("No messages to summarize");
|
|
936
|
+
}
|
|
937
|
+
logger.debug(`${this}: Generating summary for ${result.messages.length} messages ` +
|
|
938
|
+
`(index 0 to ${result.lastSummarizedIndex}), ` +
|
|
939
|
+
`target_context_tokens=${frame.targetContextTokens}`);
|
|
940
|
+
// Create summary context
|
|
941
|
+
const transcript = LLMContextSummarizationUtil.formatMessagesForSummary(result.messages);
|
|
942
|
+
const summaryContext = new LLMContext({
|
|
943
|
+
messages: [{ role: "user", content: `Conversation history:\n${transcript}` }],
|
|
944
|
+
});
|
|
945
|
+
// Generate summary using runInference. This will be overridden by each
|
|
946
|
+
// LLM service implementation.
|
|
947
|
+
let summaryText;
|
|
948
|
+
try {
|
|
949
|
+
summaryText = await this.runInference(summaryContext, {
|
|
950
|
+
maxTokens: frame.targetContextTokens,
|
|
951
|
+
systemInstruction: frame.summarizationPrompt,
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
catch (e) {
|
|
955
|
+
if (e instanceof NotImplementedError) {
|
|
956
|
+
throw new Error(`LLM service ${this.constructor.name} does not implement run_inference`);
|
|
957
|
+
}
|
|
958
|
+
throw e;
|
|
959
|
+
}
|
|
960
|
+
if (!summaryText) {
|
|
961
|
+
throw new Error("LLM returned empty summary");
|
|
962
|
+
}
|
|
963
|
+
summaryText = summaryText.trim();
|
|
964
|
+
logger.info(`${this}: Generated summary of ${summaryText.length} characters ` +
|
|
965
|
+
`for ${result.messages.length} messages`);
|
|
966
|
+
return [summaryText, result.lastSummarizedIndex];
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Register a function handler for LLM function calls.
|
|
970
|
+
*
|
|
971
|
+
* Call options resolve with the precedence **explicit argument >
|
|
972
|
+
* `toolOptions()` attributes > default**. `null`/`undefined` (the default)
|
|
973
|
+
* means "not provided" — the option falls back to the `toolOptions()` value
|
|
974
|
+
* on the handler, then to the documented default.
|
|
975
|
+
*
|
|
976
|
+
* @param functionName The name of the function to handle. Use null to handle
|
|
977
|
+
* all function calls with a catch-all handler.
|
|
978
|
+
* @param handler The function handler. Should accept a single
|
|
979
|
+
* FunctionCallParams parameter.
|
|
980
|
+
* @param options.cancelOnInterruption Whether to cancel this function call
|
|
981
|
+
* when an interruption occurs. When false the call is treated as
|
|
982
|
+
* asynchronous: the LLM continues the conversation immediately without
|
|
983
|
+
* waiting for the result, and the result is injected later via a
|
|
984
|
+
* developer message. Defaults to null (fall back to the `toolOptions()`
|
|
985
|
+
* value, then to true). A speech-to-speech service takes intermediate
|
|
986
|
+
* results (reported via `new FunctionCallResultProperties({ isFinal: false })`)
|
|
987
|
+
* on whatever second channel its provider offers; one that can't says so
|
|
988
|
+
* with `acceptsIntermediateFunctionCallResults` and drops them with a
|
|
989
|
+
* warning.
|
|
990
|
+
* @param options.timeoutSecs Optional per-tool timeout in seconds,
|
|
991
|
+
* overriding the global `functionCallTimeoutSecs`. A call that runs past it
|
|
992
|
+
* is cancelled: the handler is cancelled, the call is settled as
|
|
993
|
+
* cancelled, and inference runs so the LLM can report that it didn't
|
|
994
|
+
* complete. The deadline covers the handler's execution as a whole, so
|
|
995
|
+
* reporting an intermediate result neither clears it nor restarts it.
|
|
996
|
+
* Defaults to null (fall back to the `toolOptions()` value, then to the
|
|
997
|
+
* global timeout).
|
|
998
|
+
* @param options.cancellableByLlm Whether the LLM may cancel this call while
|
|
999
|
+
* it runs, through the `cancel_<name>` tool advertised alongside it. Pair
|
|
1000
|
+
* it with `cancelOnInterruption: false`; a synchronous call holds the LLM
|
|
1001
|
+
* until it returns, so there is no moment at which it could ask for the
|
|
1002
|
+
* call to stop. Defaults to null (fall back to the `toolOptions()` value,
|
|
1003
|
+
* then to false).
|
|
1004
|
+
*/
|
|
1005
|
+
registerFunction(functionName, handler, options = {}) {
|
|
1006
|
+
if (functionName !== null && this._cancelToolNames.has(functionName)) {
|
|
1007
|
+
throw new Error(`'${functionName}' is a reserved built-in tool name and cannot be ` +
|
|
1008
|
+
"registered by user code.");
|
|
1009
|
+
}
|
|
1010
|
+
// Explicitly registering a handler clears any standalone-unregister
|
|
1011
|
+
// suppression for its name.
|
|
1012
|
+
this._explicitlyUnregisteredFunctionNames.delete(functionName);
|
|
1013
|
+
// Registering a function with the functionName set to null will run that
|
|
1014
|
+
// handler for all functions
|
|
1015
|
+
const resolvedCancelOnInterruption = this._resolveToolOption(functionName, options.cancelOnInterruption, handler, "_pipecatCancelOnInterruption", { default: true });
|
|
1016
|
+
this._functions.set(functionName, new FunctionCallRegistryItem({
|
|
1017
|
+
functionName,
|
|
1018
|
+
handler,
|
|
1019
|
+
cancelOnInterruption: resolvedCancelOnInterruption,
|
|
1020
|
+
timeoutSecs: this._resolveToolOption(functionName, options.timeoutSecs, handler, "_pipecatTimeoutSecs", { default: null }),
|
|
1021
|
+
cancellableByLlm: this._resolveCancellableByLlm(functionName, options.cancellableByLlm, handler, resolvedCancelOnInterruption),
|
|
1022
|
+
}));
|
|
1023
|
+
this._recordToolCleanup(handler);
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Track cleanup work a registered tool handler asks to run at teardown.
|
|
1027
|
+
*
|
|
1028
|
+
* A handler backed by a resource that outlives the call — an MCP server
|
|
1029
|
+
* connection, for instance — can carry a zero-argument async callable under
|
|
1030
|
+
* `_pipecatCleanup`. It is awaited when this service is cleaned up, the hook
|
|
1031
|
+
* the pipeline guarantees to run at teardown, so the resource is released
|
|
1032
|
+
* without the developer wiring teardown manually. Handlers sharing a
|
|
1033
|
+
* resource carry the same callable, recorded once.
|
|
1034
|
+
*
|
|
1035
|
+
* Cleanups are kept for the service's lifetime even if their handlers are
|
|
1036
|
+
* later unregistered: the resource may outlive any one tool's
|
|
1037
|
+
* advertisement, and cleanups are expected to be idempotent.
|
|
1038
|
+
*/
|
|
1039
|
+
_recordToolCleanup(handler) {
|
|
1040
|
+
const cleanup = handlerAttr(handler, "_pipecatCleanup");
|
|
1041
|
+
if (cleanup === null || this._toolCleanups.includes(cleanup)) {
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
this._toolCleanups.push(cleanup);
|
|
1045
|
+
}
|
|
1046
|
+
/** Run recorded tool cleanups; errors are logged, not raised. */
|
|
1047
|
+
async _runToolCleanups() {
|
|
1048
|
+
const cleanups = this._toolCleanups;
|
|
1049
|
+
this._toolCleanups = [];
|
|
1050
|
+
for (const cleanup of cleanups) {
|
|
1051
|
+
try {
|
|
1052
|
+
await cleanup();
|
|
1053
|
+
}
|
|
1054
|
+
catch (e) {
|
|
1055
|
+
rethrowIfCancelled(e);
|
|
1056
|
+
logger.error(`${this} error running tool cleanup ${cleanup.name}: ${errorText(e)}`);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Register a direct function handler.
|
|
1062
|
+
*
|
|
1063
|
+
* Shared core behind automatic registration (from an `LLMContext` or
|
|
1064
|
+
* `LLMSetToolsFrame`). Call options resolve by precedence: explicit
|
|
1065
|
+
* argument > `toolOptions()` attributes > default.
|
|
1066
|
+
*
|
|
1067
|
+
* @param handler The direct function to register.
|
|
1068
|
+
* @param options.cancelOnInterruption Explicit override, or null to use the
|
|
1069
|
+
* handler's value (then the true default).
|
|
1070
|
+
* @param options.timeoutSecs Explicit override, or null to use the handler's
|
|
1071
|
+
* value (then the null default, i.e. the global timeout).
|
|
1072
|
+
* @param options.cancellableByLlm Explicit override, or null to use the
|
|
1073
|
+
* handler's value (then the false default).
|
|
1074
|
+
*/
|
|
1075
|
+
_registerDirectFunction(handler, options = {}) {
|
|
1076
|
+
const wrapper = new DirectFunctionWrapper(handler);
|
|
1077
|
+
if (this._cancelToolNames.has(wrapper.name)) {
|
|
1078
|
+
throw new Error(`'${wrapper.name}' is a reserved built-in tool name and cannot be ` +
|
|
1079
|
+
"registered by user code.");
|
|
1080
|
+
}
|
|
1081
|
+
const cancelOnInterruption = this._resolveToolOption(wrapper.name, options.cancelOnInterruption, handler, "_pipecatCancelOnInterruption", { default: true });
|
|
1082
|
+
const timeoutSecs = this._resolveToolOption(wrapper.name, options.timeoutSecs, handler, "_pipecatTimeoutSecs", { default: null });
|
|
1083
|
+
// Explicitly registering a handler clears any standalone-unregister
|
|
1084
|
+
// suppression for its name. (Auto-registration skips suppressed names
|
|
1085
|
+
// before reaching this method, so this only fires for explicit calls.)
|
|
1086
|
+
this._explicitlyUnregisteredFunctionNames.delete(wrapper.name);
|
|
1087
|
+
// The new entry defaults to autoRegistered=false, so a handler registered
|
|
1088
|
+
// through this method is treated as explicit and is never auto-pruned.
|
|
1089
|
+
this._functions.set(wrapper.name, new FunctionCallRegistryItem({
|
|
1090
|
+
functionName: wrapper.name,
|
|
1091
|
+
handler: wrapper,
|
|
1092
|
+
cancelOnInterruption,
|
|
1093
|
+
timeoutSecs,
|
|
1094
|
+
cancellableByLlm: this._resolveCancellableByLlm(wrapper.name, options.cancellableByLlm, handler, cancelOnInterruption),
|
|
1095
|
+
}));
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Resolve whether the LLM may cancel a tool's calls.
|
|
1099
|
+
*
|
|
1100
|
+
* @param functionName The tool's name, for the warning below.
|
|
1101
|
+
* @param explicit The value passed to the register call, or null if omitted.
|
|
1102
|
+
* @param handler The handler, which may carry a `toolOptions()` value.
|
|
1103
|
+
* @param cancelOnInterruption The tool's interruption behaviour, already
|
|
1104
|
+
* resolved by the caller — resolving it again here would read past
|
|
1105
|
+
* whatever the caller was told explicitly.
|
|
1106
|
+
* @returns Whether the LLM may cancel this tool's calls.
|
|
1107
|
+
*/
|
|
1108
|
+
_resolveCancellableByLlm(functionName, explicit, handler, cancelOnInterruption) {
|
|
1109
|
+
const resolved = this._resolveToolOption(functionName, explicit, handler, "_pipecatCancellableByLlm", { default: false });
|
|
1110
|
+
if (!resolved) {
|
|
1111
|
+
return false;
|
|
1112
|
+
}
|
|
1113
|
+
if (cancelOnInterruption) {
|
|
1114
|
+
// A synchronous call holds the LLM until it returns, so it is never
|
|
1115
|
+
// running at a moment the LLM could ask to cancel it.
|
|
1116
|
+
logger.warning(`${this}: '${functionName}' sets cancellable_by_llm=True with ` +
|
|
1117
|
+
"cancel_on_interruption=True. The LLM cannot cancel a synchronous " +
|
|
1118
|
+
"call; pair it with cancel_on_interruption=False.");
|
|
1119
|
+
return false;
|
|
1120
|
+
}
|
|
1121
|
+
return true;
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Resolve a tool call option by precedence: explicit > handler attribute >
|
|
1125
|
+
* default.
|
|
1126
|
+
*
|
|
1127
|
+
* An explicit `null` is treated as "not provided" and falls back to the
|
|
1128
|
+
* handler's value, then the default — so `null` can't be passed to force a
|
|
1129
|
+
* default past a handler value (an intentionally unsupported niche; set
|
|
1130
|
+
* `functionCallTimeoutSecs` on the service for that).
|
|
1131
|
+
*
|
|
1132
|
+
* @param functionName The tool's name, for logging.
|
|
1133
|
+
* @param explicit The value passed to the register call, or null if the
|
|
1134
|
+
* caller omitted it.
|
|
1135
|
+
* @param handler The handler, which may carry `toolOptions()` attributes.
|
|
1136
|
+
* @param attr The attribute to read (e.g. `_pipecatTimeoutSecs`).
|
|
1137
|
+
* @param options.default The value to use when neither an explicit argument
|
|
1138
|
+
* nor a handler value is present.
|
|
1139
|
+
* @returns The resolved option value.
|
|
1140
|
+
*/
|
|
1141
|
+
_resolveToolOption(functionName, explicit, handler, attr, options) {
|
|
1142
|
+
const decorated = handlerAttr(handler, attr);
|
|
1143
|
+
if (explicit !== null && explicit !== undefined) {
|
|
1144
|
+
if (decorated !== null && decorated !== explicit) {
|
|
1145
|
+
const optionName = TOOL_OPTION_NAMES[attr] ?? attr.replace(/^_pipecat/, "");
|
|
1146
|
+
logger.debug(`${this}: '${functionName}' registered with explicit ` +
|
|
1147
|
+
`${optionName}=${repr(explicit)}, overriding the ` +
|
|
1148
|
+
`decorator value ${repr(decorated)}.`);
|
|
1149
|
+
}
|
|
1150
|
+
return explicit;
|
|
1151
|
+
}
|
|
1152
|
+
return decorated !== null ? decorated : options.default;
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Whether an advertised handler differs from the one already registered.
|
|
1156
|
+
*
|
|
1157
|
+
* Normalizes a `DirectFunctionWrapper` to its underlying `.function` on both
|
|
1158
|
+
* sides, then compares by identity. Flows reuses one closure per node across
|
|
1159
|
+
* repeated context frames (no change) but builds a fresh closure on
|
|
1160
|
+
* transition (a real change), so identity is the right test.
|
|
1161
|
+
*
|
|
1162
|
+
* @param item The currently registered entry.
|
|
1163
|
+
* @param newHandler The advertised handler — a raw callable or a
|
|
1164
|
+
* `DirectFunctionWrapper`.
|
|
1165
|
+
* @returns True if `newHandler` is a different callable than `item` holds.
|
|
1166
|
+
*/
|
|
1167
|
+
_advertisedHandlerChanged(item, newHandler) {
|
|
1168
|
+
const current = item.handler instanceof DirectFunctionWrapper ? item.handler.function : item.handler;
|
|
1169
|
+
const next = newHandler instanceof DirectFunctionWrapper ? newHandler.function : newHandler;
|
|
1170
|
+
return current !== next;
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Register handlers for any tools in the given set that carry one.
|
|
1174
|
+
*
|
|
1175
|
+
* A tool carries a handler when it's advertised as a direct function or as
|
|
1176
|
+
* a `FunctionSchema` with a `handler` set; either way the handler is
|
|
1177
|
+
* registered automatically, so no `registerFunction` call is needed. A
|
|
1178
|
+
* `FunctionSchema` without a handler is advertise-only and registers
|
|
1179
|
+
* nothing.
|
|
1180
|
+
*
|
|
1181
|
+
* Accepts whatever `LLMContext` accepts for tools — a `ToolsSchema`, a plain
|
|
1182
|
+
* list of direct functions / `FunctionSchema` objects, or `NOT_GIVEN` —
|
|
1183
|
+
* normalizing as needed.
|
|
1184
|
+
*
|
|
1185
|
+
* Explicit `registerFunction` registrations are always left untouched, so
|
|
1186
|
+
* explicit registration wins. An auto-registered entry (from a previous
|
|
1187
|
+
* advertised set) is rebound when the new set carries a *different* handler
|
|
1188
|
+
* for the same name — so a re-declared per-node handler takes effect — and
|
|
1189
|
+
* left untouched when the handler is unchanged, so repeated frames don't
|
|
1190
|
+
* churn.
|
|
1191
|
+
*
|
|
1192
|
+
* @param tools The tools to scan for handlers.
|
|
1193
|
+
*/
|
|
1194
|
+
_registerAdvertisedToolHandlers(tools) {
|
|
1195
|
+
// A context's `tools` may be null (rather than `NOT_GIVEN`) — e.g. from
|
|
1196
|
+
// realtime services or stand-in contexts in tests. isGiven(null) is true
|
|
1197
|
+
// and the normalizer rejects null, so guard it explicitly.
|
|
1198
|
+
if (tools === null || tools === undefined) {
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
const normalized = LLMContext._normalizeAndValidateTools(tools);
|
|
1202
|
+
if (!isGiven(normalized)) {
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
// Register direct functions.
|
|
1206
|
+
for (const wrapper of normalized.directFunctions) {
|
|
1207
|
+
const existing = this._functions.get(wrapper.name);
|
|
1208
|
+
if (existing !== undefined) {
|
|
1209
|
+
// An auto-registered entry whose advertised handler changed is
|
|
1210
|
+
// rebound to the new handler. Explicit registrations and unchanged
|
|
1211
|
+
// handlers are left untouched, so explicit wins and repeated frames
|
|
1212
|
+
// don't churn.
|
|
1213
|
+
if (existing.autoRegistered && this._advertisedHandlerChanged(existing, wrapper.function)) {
|
|
1214
|
+
this._registerDirectFunction(wrapper.function);
|
|
1215
|
+
this._markAutoRegistered(wrapper.name);
|
|
1216
|
+
logger.debug(`${this}: rebound advertised direct function '${wrapper.name}' to its new handler`);
|
|
1217
|
+
}
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
if (this._explicitlyUnregisteredFunctionNames.has(wrapper.name)) {
|
|
1221
|
+
// Explicitly unregistered while still advertised — leave it gone so
|
|
1222
|
+
// calls hit the missing-handler recovery path.
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
this._registerDirectFunction(wrapper.function);
|
|
1226
|
+
// Mark the entry as advertised-tool-set-managed so it can be pruned on a
|
|
1227
|
+
// later sync that stops advertising it. Explicit registrations are
|
|
1228
|
+
// handled above and keep their default autoRegistered=false, so they
|
|
1229
|
+
// are never pruned.
|
|
1230
|
+
this._markAutoRegistered(wrapper.name);
|
|
1231
|
+
logger.debug(`${this}: auto-registered handler for advertised direct function '${wrapper.name}'`);
|
|
1232
|
+
}
|
|
1233
|
+
// Register the handlers that FunctionSchemas carry. A schema handler
|
|
1234
|
+
// registers like any classic handler — registerFunction reads its
|
|
1235
|
+
// toolOptions() attributes off the handler — only marked autoRegistered
|
|
1236
|
+
// so it's pruned when no longer advertised.
|
|
1237
|
+
for (const schema of normalized.standardTools) {
|
|
1238
|
+
if (schema.handler === null || schema.handler === undefined) {
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
const existing = this._functions.get(schema.name);
|
|
1242
|
+
if (existing !== undefined) {
|
|
1243
|
+
// Rebind an auto-registered entry whose advertised handler changed;
|
|
1244
|
+
// otherwise leave it (explicit wins, unchanged handlers don't churn)
|
|
1245
|
+
// and warn if a manual registration shadows a handler-carrying schema.
|
|
1246
|
+
if (existing.autoRegistered && this._advertisedHandlerChanged(existing, schema.handler)) {
|
|
1247
|
+
this.registerFunction(schema.name, schema.handler);
|
|
1248
|
+
this._markAutoRegistered(schema.name);
|
|
1249
|
+
logger.debug(`${this}: rebound advertised FunctionSchema '${schema.name}' to its new handler`);
|
|
1250
|
+
}
|
|
1251
|
+
else {
|
|
1252
|
+
this._warnIfRedundantManualRegistration(schema.name);
|
|
1253
|
+
}
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
if (this._explicitlyUnregisteredFunctionNames.has(schema.name)) {
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
this.registerFunction(schema.name, schema.handler);
|
|
1260
|
+
this._markAutoRegistered(schema.name);
|
|
1261
|
+
logger.debug(`${this}: auto-registered handler for advertised FunctionSchema '${schema.name}'`);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
/** Mark a registry entry as managed by the advertised tool set. */
|
|
1265
|
+
_markAutoRegistered(name) {
|
|
1266
|
+
const item = this._functions.get(name);
|
|
1267
|
+
if (item)
|
|
1268
|
+
item.autoRegistered = true;
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Warn that a manual registration is unnecessary, once per function.
|
|
1272
|
+
*
|
|
1273
|
+
* Fires when a tool is registered explicitly via `registerFunction` yet its
|
|
1274
|
+
* advertised `FunctionSchema` already carries a handler — the schema alone
|
|
1275
|
+
* would register it. An auto-registered entry (e.g. from a same-named
|
|
1276
|
+
* direct function) is not a manual registration and is left silent.
|
|
1277
|
+
*
|
|
1278
|
+
* @param functionName The name shared by the manual registration and the
|
|
1279
|
+
* handler-carrying schema. Must already be registered.
|
|
1280
|
+
*/
|
|
1281
|
+
_warnIfRedundantManualRegistration(functionName) {
|
|
1282
|
+
if (this._functions.get(functionName)?.autoRegistered) {
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
if (this._redundantRegistrationWarned.has(functionName)) {
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
this._redundantRegistrationWarned.add(functionName);
|
|
1289
|
+
logger.warning(`${this}: '${functionName}' is registered with register_function() but its ` +
|
|
1290
|
+
"advertised FunctionSchema already carries a handler. The manual registration " +
|
|
1291
|
+
"step is unnecessary when the handler is on the FunctionSchema.");
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Return the service's own configured tools, if any.
|
|
1295
|
+
*
|
|
1296
|
+
* Used by the auto-registration mechanism. Tools normally reach a service
|
|
1297
|
+
* through the `LLMContext` on each inference. Some services (the realtime
|
|
1298
|
+
* services) also accept tools configured directly on the service (e.g. via
|
|
1299
|
+
* a constructor option) as an alternative; they override this to return
|
|
1300
|
+
* those tools verbatim, in whatever form they hold them — a `ToolsSchema` or
|
|
1301
|
+
* a provider-native tool list. When the context advertises no tools,
|
|
1302
|
+
* `_syncRegisteredToolHandlers` normalizes the result and registers handlers
|
|
1303
|
+
* for any standard tools it contains; provider-native tools carry no
|
|
1304
|
+
* handlers, so they contribute nothing to register.
|
|
1305
|
+
*
|
|
1306
|
+
* A service that overrides this **must follow the auto-registration
|
|
1307
|
+
* mechanism's preference rules: context tools, then service-configured
|
|
1308
|
+
* tools**.
|
|
1309
|
+
*
|
|
1310
|
+
* @returns The service's configured tools verbatim (e.g. a `ToolsSchema` or
|
|
1311
|
+
* a provider-native tool list), or null when there are none (the default).
|
|
1312
|
+
*/
|
|
1313
|
+
_serviceTools() {
|
|
1314
|
+
return null;
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Sync the registered handlers with the handlers advertised in `tools`.
|
|
1318
|
+
*
|
|
1319
|
+
* Registers handlers for any tools the set advertises with one (direct
|
|
1320
|
+
* functions, or `FunctionSchema` objects carrying a `handler`), then drops
|
|
1321
|
+
* the handlers we auto-registered for tools it no longer advertises — so
|
|
1322
|
+
* the registry matches what the LLM can see.
|
|
1323
|
+
*
|
|
1324
|
+
* This is the single path for keeping handlers in step with the advertised
|
|
1325
|
+
* tools. The base service runs it on every `LLMContextFrame` (the context
|
|
1326
|
+
* carries the current tool set on each inference). Realtime services that
|
|
1327
|
+
* support runtime tool changes also call it from their own
|
|
1328
|
+
* `LLMSetToolsFrame` handling, since they run continuously and don't get a
|
|
1329
|
+
* context frame per turn. When the context advertises no tools, handlers
|
|
1330
|
+
* fall back to `_serviceTools()` (the service's own configured tools).
|
|
1331
|
+
*
|
|
1332
|
+
* Explicit registrations (`registerFunction`), the catch-all handler, and
|
|
1333
|
+
* built-in tools are never pruned.
|
|
1334
|
+
*
|
|
1335
|
+
* @param tools The advertised tool set (a `ToolsSchema`, a plain list, or
|
|
1336
|
+
* null / `NOT_GIVEN` for "no tools").
|
|
1337
|
+
*/
|
|
1338
|
+
_syncRegisteredToolHandlers(tools) {
|
|
1339
|
+
// null and an empty list both mean "no tools advertised" (the normalizer
|
|
1340
|
+
// collapses an empty set to NOT_GIVEN), in which case every
|
|
1341
|
+
// auto-registered handler is pruned.
|
|
1342
|
+
let normalized = tools !== null && tools !== undefined
|
|
1343
|
+
? LLMContext._normalizeAndValidateTools(tools)
|
|
1344
|
+
: NOT_GIVEN;
|
|
1345
|
+
// No context tools? Fall back to the service's own configured tools.
|
|
1346
|
+
// Those may be provider-native (already formatted, carrying no handlers);
|
|
1347
|
+
// only standard tools (gathered into a ToolsSchema) contribute handlers.
|
|
1348
|
+
if (!isGiven(normalized)) {
|
|
1349
|
+
const serviceTools = this._serviceTools();
|
|
1350
|
+
if (serviceTools !== null) {
|
|
1351
|
+
const normalizedServiceTools = LLMContext._normalizeAndValidateTools(serviceTools, {
|
|
1352
|
+
allowProviderTools: true,
|
|
1353
|
+
});
|
|
1354
|
+
if (normalizedServiceTools instanceof ToolsSchema) {
|
|
1355
|
+
normalized = normalizedServiceTools;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
this._registerAdvertisedToolHandlers(normalized);
|
|
1360
|
+
const advertised = new Set();
|
|
1361
|
+
if (normalized instanceof ToolsSchema) {
|
|
1362
|
+
for (const wrapper of normalized.directFunctions)
|
|
1363
|
+
advertised.add(wrapper.name);
|
|
1364
|
+
for (const s of normalized.standardTools) {
|
|
1365
|
+
if (s.handler !== null && s.handler !== undefined)
|
|
1366
|
+
advertised.add(s.name);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
this._unregisterUnadvertisedToolHandlers(advertised);
|
|
1370
|
+
// A standalone unregister only suppresses re-registration while the tool
|
|
1371
|
+
// is still advertised. Once it leaves the advertised set, drop the
|
|
1372
|
+
// suppression so re-advertising it (a later tool-set change) registers it
|
|
1373
|
+
// afresh.
|
|
1374
|
+
for (const name of [...this._explicitlyUnregisteredFunctionNames]) {
|
|
1375
|
+
if (!advertised.has(name))
|
|
1376
|
+
this._explicitlyUnregisteredFunctionNames.delete(name);
|
|
1377
|
+
}
|
|
1378
|
+
// What the registry holds decides both whether the async-tool guidance
|
|
1379
|
+
// belongs in the system instruction and whether the cancellation tools
|
|
1380
|
+
// do, and it is settled only here: tools advertised on a context frame
|
|
1381
|
+
// register during this sync, a manual registration never goes through
|
|
1382
|
+
// that path at all, and a context can advertise nothing.
|
|
1383
|
+
this._syncCancelTools();
|
|
1384
|
+
this._composeSystemInstruction();
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Drop auto-registered handlers for tools no longer advertised.
|
|
1388
|
+
*
|
|
1389
|
+
* Only entries with `autoRegistered: true` are eligible; explicit
|
|
1390
|
+
* registrations, the catch-all handler, and built-in tools are untouched.
|
|
1391
|
+
*
|
|
1392
|
+
* @param advertised Names of handler-carrying tools in the new advertised set.
|
|
1393
|
+
*/
|
|
1394
|
+
_unregisterUnadvertisedToolHandlers(advertised) {
|
|
1395
|
+
const stale = [...this._functions.entries()]
|
|
1396
|
+
.filter(([name, item]) => item.autoRegistered && !advertised.has(name))
|
|
1397
|
+
.map(([name]) => name);
|
|
1398
|
+
for (const name of stale) {
|
|
1399
|
+
this._functions.delete(name);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Remove a registered function handler.
|
|
1404
|
+
*
|
|
1405
|
+
* This removes the handler but does not stop advertising the tool to the
|
|
1406
|
+
* LLM. To remove a tool cleanly, prefer an `LLMSetToolsFrame` with the
|
|
1407
|
+
* updated tool set — that both stops advertising it and avoids the LLM
|
|
1408
|
+
* trying to call a tool that's no longer there.
|
|
1409
|
+
*
|
|
1410
|
+
* @param functionName The name of the function handler to remove.
|
|
1411
|
+
* @throws Error if no handler is registered under that name.
|
|
1412
|
+
*/
|
|
1413
|
+
unregisterFunction(functionName) {
|
|
1414
|
+
if (!this._functions.delete(functionName)) {
|
|
1415
|
+
throw new Error(`KeyError: ${repr(functionName)}`);
|
|
1416
|
+
}
|
|
1417
|
+
// Remember the explicit removal so auto-registration doesn't bring the
|
|
1418
|
+
// handler back on the next context frame while the tool is still
|
|
1419
|
+
// advertised.
|
|
1420
|
+
this._explicitlyUnregisteredFunctionNames.add(functionName);
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Remove a registered direct function handler (internal; no warning).
|
|
1424
|
+
*
|
|
1425
|
+
* @param handler The direct function handler to remove.
|
|
1426
|
+
* @throws Error if no handler is registered under its name.
|
|
1427
|
+
*/
|
|
1428
|
+
_unregisterDirectFunction(handler) {
|
|
1429
|
+
const wrapper = new DirectFunctionWrapper(handler);
|
|
1430
|
+
if (!this._functions.delete(wrapper.name)) {
|
|
1431
|
+
throw new Error(`KeyError: ${repr(wrapper.name)}`);
|
|
1432
|
+
}
|
|
1433
|
+
// Remember the explicit removal so auto-registration doesn't bring the
|
|
1434
|
+
// handler back on the next context frame while the tool is still advertised.
|
|
1435
|
+
this._explicitlyUnregisteredFunctionNames.add(wrapper.name);
|
|
1436
|
+
// Note: no need to remove start callback here, as direct functions don't support start callbacks.
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Check if a function handler is registered.
|
|
1440
|
+
*
|
|
1441
|
+
* @param functionName The name of the function to check.
|
|
1442
|
+
* @returns True if the function is registered or if a catch-all handler
|
|
1443
|
+
* (null) is registered.
|
|
1444
|
+
*/
|
|
1445
|
+
hasFunction(functionName) {
|
|
1446
|
+
if (this._functions.has(null)) {
|
|
1447
|
+
return true;
|
|
1448
|
+
}
|
|
1449
|
+
return this._functions.has(functionName);
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Whether the named function was registered with cancelOnInterruption=false.
|
|
1453
|
+
*
|
|
1454
|
+
* Mirrors the registry-lookup pattern in `runFunctionCalls()`: a
|
|
1455
|
+
* name-specific entry takes precedence; if there isn't one, fall back to
|
|
1456
|
+
* the null-keyed catch-all entry. Returns false if no entry matches.
|
|
1457
|
+
*/
|
|
1458
|
+
_functionIsAsync(functionName) {
|
|
1459
|
+
const item = this._functions.get(functionName) ?? this._functions.get(null);
|
|
1460
|
+
return item !== undefined && !item.cancelOnInterruption;
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Execute a sequence of function calls from the LLM.
|
|
1464
|
+
*
|
|
1465
|
+
* Triggers the on_function_calls_started event and executes functions
|
|
1466
|
+
* either in parallel or sequentially based on the runInParallel setting.
|
|
1467
|
+
*
|
|
1468
|
+
* @param functionCalls The function calls to execute.
|
|
1469
|
+
*/
|
|
1470
|
+
async runFunctionCalls(functionCalls) {
|
|
1471
|
+
if (functionCalls.length === 0) {
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
if (this._speculationGate.isSpeculating) {
|
|
1475
|
+
// Tools run inside the service, so no downstream gate can undo their
|
|
1476
|
+
// side effects if the speculation is discarded. Withdraw it instead;
|
|
1477
|
+
// the inference that follows the committed transcript runs the call. A
|
|
1478
|
+
// turn confirmed before the call was reached leaves nothing pending,
|
|
1479
|
+
// and the call runs as an ordinary one.
|
|
1480
|
+
logger.debug(`${this}: speculative inference wants a tool call, cancelling it`);
|
|
1481
|
+
await this.broadcastFrame(EagerEndOfTurnCancelFrame);
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
// Exclude the built-in cancel tool — it's an internal mechanism and
|
|
1485
|
+
// should not be surfaced to user-facing event handlers or frames.
|
|
1486
|
+
const userVisibleCalls = functionCalls.filter((fc) => !this._cancelToolNames.has(fc.functionName));
|
|
1487
|
+
if (userVisibleCalls.length > 0) {
|
|
1488
|
+
await this._callEventHandler("on_function_calls_started", userVisibleCalls);
|
|
1489
|
+
await this.broadcastFrame(FunctionCallsStartedFrame, { functionCalls: userVisibleCalls });
|
|
1490
|
+
}
|
|
1491
|
+
// When groupParallelTools is true all calls share a groupId so the
|
|
1492
|
+
// aggregator triggers the LLM exactly once after the last one completes.
|
|
1493
|
+
// When false, groupId is null and each result triggers inference
|
|
1494
|
+
// independently.
|
|
1495
|
+
const groupId = this._groupParallelTools ? crypto.randomUUID() : null;
|
|
1496
|
+
const runnerItems = [];
|
|
1497
|
+
for (const functionCall of functionCalls) {
|
|
1498
|
+
let item = this._functions.get(functionCall.functionName) ?? this._functions.get(null);
|
|
1499
|
+
if (item === undefined) {
|
|
1500
|
+
this._logMissingFunctionCall(functionCall.functionName, functionCall.context);
|
|
1501
|
+
item = this._buildMissingFunctionCallRegistryItem(functionCall.functionName);
|
|
1502
|
+
}
|
|
1503
|
+
runnerItems.push(new FunctionCallRunnerItem({
|
|
1504
|
+
registryItem: item,
|
|
1505
|
+
functionName: functionCall.functionName,
|
|
1506
|
+
toolCallId: functionCall.toolCallId,
|
|
1507
|
+
arguments: functionCall.arguments,
|
|
1508
|
+
context: functionCall.context,
|
|
1509
|
+
groupId,
|
|
1510
|
+
}));
|
|
1511
|
+
}
|
|
1512
|
+
if (this._runInParallel) {
|
|
1513
|
+
await this._runParallelFunctionCalls(runnerItems);
|
|
1514
|
+
}
|
|
1515
|
+
else {
|
|
1516
|
+
await this._runSequentialFunctionCalls(runnerItems);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
async _createSequentialRunnerTask() {
|
|
1520
|
+
if (!this._sequentialRunnerTask) {
|
|
1521
|
+
this._sequentialRunnerQueue = new Queue();
|
|
1522
|
+
this._sequentialRunnerTask = this.createTask(() => this._sequentialRunnerHandler(), "_sequential_runner_handler");
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
async _cancelSequentialRunnerTask() {
|
|
1526
|
+
if (this._sequentialRunnerTask) {
|
|
1527
|
+
await this.cancelTask(this._sequentialRunnerTask);
|
|
1528
|
+
this._sequentialRunnerTask = null;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
async _cancelSummaryTask() {
|
|
1532
|
+
if (this._summaryTask) {
|
|
1533
|
+
await this.cancelTask(this._summaryTask);
|
|
1534
|
+
this._summaryTask = null;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
async _cancelAllFunctionCallTasks() {
|
|
1538
|
+
// Snapshot first: cancelTask awaits, during which done callbacks may
|
|
1539
|
+
// mutate _functionCallTasks.
|
|
1540
|
+
for (const task of [...this._functionCallTasks.keys()]) {
|
|
1541
|
+
if (task) {
|
|
1542
|
+
task.removeDoneCallback(this.#functionCallTaskDone);
|
|
1543
|
+
await this.cancelTask(task);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
this._functionCallTasks.clear();
|
|
1547
|
+
}
|
|
1548
|
+
async _sequentialRunnerHandler() {
|
|
1549
|
+
while (true) {
|
|
1550
|
+
const runnerItem = await this._sequentialRunnerQueue.get();
|
|
1551
|
+
const task = this.createTask(() => this._runFunctionCall(runnerItem), "_run_function_call");
|
|
1552
|
+
this._functionCallTasks.set(task, runnerItem);
|
|
1553
|
+
// Since we run tasks sequentially we don't need to add the
|
|
1554
|
+
// _functionCallTaskFinished done callback.
|
|
1555
|
+
try {
|
|
1556
|
+
// Wait on the task rather than awaiting it directly: a function call
|
|
1557
|
+
// cancelled from elsewhere (interruption, timeout) must not tear down
|
|
1558
|
+
// the runner along with it.
|
|
1559
|
+
await wait([task]);
|
|
1560
|
+
}
|
|
1561
|
+
finally {
|
|
1562
|
+
// Waiting doesn't hand cancellation down the way awaiting the task
|
|
1563
|
+
// would, so a runner going away has to cancel the call it was running
|
|
1564
|
+
// itself.
|
|
1565
|
+
if (!task.done()) {
|
|
1566
|
+
await this.cancelTask(task);
|
|
1567
|
+
}
|
|
1568
|
+
// The canceller may already have dropped the entry.
|
|
1569
|
+
this._functionCallTasks.delete(task);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
async _runParallelFunctionCalls(runnerItems) {
|
|
1574
|
+
for (const runnerItem of runnerItems) {
|
|
1575
|
+
const task = this.createTask(() => this._runFunctionCall(runnerItem), "_run_function_call");
|
|
1576
|
+
this._functionCallTasks.set(task, runnerItem);
|
|
1577
|
+
task.addDoneCallback(this.#functionCallTaskDone);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
async _runSequentialFunctionCalls(runnerItems) {
|
|
1581
|
+
// Enqueue all function calls for background execution.
|
|
1582
|
+
for (const runnerItem of runnerItems) {
|
|
1583
|
+
await this._sequentialRunnerQueue.put(runnerItem);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
async _runFunctionCall(runnerItem) {
|
|
1587
|
+
// Re-resolve the registry item at execution time. The function may have
|
|
1588
|
+
// been unregistered between queuing and execution, in which case we fall
|
|
1589
|
+
// back to the missing-function handler so the call still terminates with
|
|
1590
|
+
// a normal tool result.
|
|
1591
|
+
let item;
|
|
1592
|
+
const named = this._functions.get(runnerItem.functionName);
|
|
1593
|
+
const catchAll = this._functions.get(null);
|
|
1594
|
+
if (named !== undefined) {
|
|
1595
|
+
item = named;
|
|
1596
|
+
}
|
|
1597
|
+
else if (catchAll !== undefined) {
|
|
1598
|
+
item = catchAll;
|
|
1599
|
+
}
|
|
1600
|
+
else if (runnerItem.registryItem.handler === this.#missingFunctionCallHandler) {
|
|
1601
|
+
item = runnerItem.registryItem;
|
|
1602
|
+
}
|
|
1603
|
+
else {
|
|
1604
|
+
// Function was unregistered between queue and execution; the
|
|
1605
|
+
// registry-item-handler check above already covered the
|
|
1606
|
+
// missing-from-the-start case.
|
|
1607
|
+
logger.warning(`${this}: '${runnerItem.functionName}' was just unregistered ` +
|
|
1608
|
+
"between queueing and execution.");
|
|
1609
|
+
item = this._buildMissingFunctionCallRegistryItem(runnerItem.functionName);
|
|
1610
|
+
}
|
|
1611
|
+
logger.debug(`${this} Calling function [${runnerItem.functionName}:${runnerItem.toolCallId}] with arguments ${JSON.stringify(runnerItem.arguments)}`);
|
|
1612
|
+
// Broadcast function call in-progress. This frame will let our assistant
|
|
1613
|
+
// context aggregator know that we are in the middle of a function call.
|
|
1614
|
+
// Some contexts/aggregators may not need this. But some definitely do
|
|
1615
|
+
// (Anthropic, for example).
|
|
1616
|
+
await this.broadcastFrame(FunctionCallInProgressFrame, {
|
|
1617
|
+
functionName: runnerItem.functionName,
|
|
1618
|
+
toolCallId: runnerItem.toolCallId,
|
|
1619
|
+
arguments: runnerItem.arguments,
|
|
1620
|
+
cancelOnInterruption: item.cancelOnInterruption,
|
|
1621
|
+
groupId: runnerItem.groupId,
|
|
1622
|
+
});
|
|
1623
|
+
let timeoutTask = null;
|
|
1624
|
+
// Set when the handler raises, so the result settling the call on its
|
|
1625
|
+
// behalf can say what went wrong.
|
|
1626
|
+
let callError = null;
|
|
1627
|
+
// Single callback for both intermediate updates and final results. Pass
|
|
1628
|
+
// properties: new FunctionCallResultProperties({ isFinal: false }) for updates.
|
|
1629
|
+
const functionCallResultCallback = async (result, options = {}) => {
|
|
1630
|
+
const properties = options.properties ?? null;
|
|
1631
|
+
const isFinal = properties ? properties.isFinal : true;
|
|
1632
|
+
if (!isFinal && item.cancelOnInterruption) {
|
|
1633
|
+
logger.warning(`${this} result_callback called with is_final=False on sync function call` +
|
|
1634
|
+
` [${runnerItem.functionName}:${runnerItem.toolCallId}].` +
|
|
1635
|
+
" Intermediate updates are only valid for async function calls" +
|
|
1636
|
+
" (cancel_on_interruption=False).");
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
// A handler can outlive its call and still hold this callback, so late
|
|
1640
|
+
// results are rejected here rather than broadcast for the aggregator to
|
|
1641
|
+
// drop.
|
|
1642
|
+
if (runnerItem.settled) {
|
|
1643
|
+
logger.warning(`${this} Ignoring result for function call` +
|
|
1644
|
+
` [${runnerItem.functionName}:${runnerItem.toolCallId}]: the call has` +
|
|
1645
|
+
" already finished (it reported a result, timed out, or was cancelled).");
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
if (isFinal) {
|
|
1649
|
+
runnerItem.settled = true;
|
|
1650
|
+
// Only a final result settles the call, so only a final result clears
|
|
1651
|
+
// the deadline. An intermediate update must leave it armed —
|
|
1652
|
+
// otherwise a progress report permanently unbounds a hanging async
|
|
1653
|
+
// tool.
|
|
1654
|
+
if (timeoutTask && !timeoutTask.done()) {
|
|
1655
|
+
await this.cancelTask(timeoutTask);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
await this.broadcastFrame(FunctionCallResultFrame, {
|
|
1659
|
+
functionName: runnerItem.functionName,
|
|
1660
|
+
toolCallId: runnerItem.toolCallId,
|
|
1661
|
+
arguments: runnerItem.arguments,
|
|
1662
|
+
result,
|
|
1663
|
+
runLlm: runnerItem.runLlm,
|
|
1664
|
+
properties,
|
|
1665
|
+
error: callError,
|
|
1666
|
+
});
|
|
1667
|
+
};
|
|
1668
|
+
// Python `or`: a 0 per-tool timeout falls back to the global one.
|
|
1669
|
+
const effectiveTimeout = item.timeoutSecs || this._functionCallTimeoutSecs;
|
|
1670
|
+
// Start a timeout task for deferred function calls
|
|
1671
|
+
const timeoutHandler = async () => {
|
|
1672
|
+
// This task is only started when one of the two is set.
|
|
1673
|
+
const timeout = effectiveTimeout;
|
|
1674
|
+
await sleep(timeout);
|
|
1675
|
+
logger.warning(`${this} Function call [${runnerItem.functionName}:${runnerItem.toolCallId}] timed out after ${timeout} seconds and is being cancelled.` +
|
|
1676
|
+
" You can increase this timeout by passing `timeout_secs` to `register_function()`," +
|
|
1677
|
+
" or set a global default via `function_call_timeout_secs` on the LLM constructor.");
|
|
1678
|
+
// Settle the call before cancelling, so a result racing in while the
|
|
1679
|
+
// handler unwinds is rejected instead of broadcast.
|
|
1680
|
+
runnerItem.settled = true;
|
|
1681
|
+
// Cancelling goes through a detached task because it awaits the
|
|
1682
|
+
// function call task this handler runs inside of.
|
|
1683
|
+
this.createTask(() => this._timeoutFunctionCall(runnerItem), "_timeout_function_call");
|
|
1684
|
+
};
|
|
1685
|
+
if (effectiveTimeout) {
|
|
1686
|
+
timeoutTask = this.createTask(timeoutHandler, "timeout_handler");
|
|
1687
|
+
}
|
|
1688
|
+
try {
|
|
1689
|
+
const params = new FunctionCallParams({
|
|
1690
|
+
functionName: runnerItem.functionName,
|
|
1691
|
+
toolCallId: runnerItem.toolCallId,
|
|
1692
|
+
arguments: runnerItem.arguments,
|
|
1693
|
+
llm: this,
|
|
1694
|
+
pipelineWorker: this.pipelineWorker,
|
|
1695
|
+
context: runnerItem.context,
|
|
1696
|
+
resultCallback: functionCallResultCallback,
|
|
1697
|
+
appResources: this.pipelineWorker.appResources,
|
|
1698
|
+
workerRunner: this.pipelineWorker.workerRunner,
|
|
1699
|
+
});
|
|
1700
|
+
if (item.handler instanceof DirectFunctionWrapper) {
|
|
1701
|
+
// Handler is a DirectFunctionWrapper
|
|
1702
|
+
await item.handler.invoke(runnerItem.arguments, params);
|
|
1703
|
+
}
|
|
1704
|
+
else {
|
|
1705
|
+
// Handler is a FunctionCallHandler
|
|
1706
|
+
await item.handler(params);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
catch (e) {
|
|
1710
|
+
if (e instanceof CancelledError) {
|
|
1711
|
+
// Backstop for cancellation that doesn't run through
|
|
1712
|
+
// _cancelFunctionCallTasks, e.g. teardown.
|
|
1713
|
+
runnerItem.settled = true;
|
|
1714
|
+
throw e;
|
|
1715
|
+
}
|
|
1716
|
+
const errorMessage = `Error executing function call [${runnerItem.functionName}]: ${errorText(e)}`;
|
|
1717
|
+
// Pushing the error with category "APPLICATION" since the failure came
|
|
1718
|
+
// from the application's function handler code.
|
|
1719
|
+
await this.pushError(errorMessage, { exception: e, category: ErrorCategory.APPLICATION });
|
|
1720
|
+
// A handler that raised will never report, so settle the call on its
|
|
1721
|
+
// behalf.
|
|
1722
|
+
callError = e instanceof Error ? `${e.name}: ${e.message}` : `Error: ${String(e)}`;
|
|
1723
|
+
const cls = this.constructor;
|
|
1724
|
+
await functionCallResultCallback(cls.FUNCTION_CALL_ERROR_MESSAGE_TEMPLATE.replaceAll("{function_name}", runnerItem.functionName));
|
|
1725
|
+
}
|
|
1726
|
+
finally {
|
|
1727
|
+
if (timeoutTask && !timeoutTask.done()) {
|
|
1728
|
+
await this.cancelTask(timeoutTask);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
/** Build a registry item that routes to the missing-function handler. */
|
|
1733
|
+
_buildMissingFunctionCallRegistryItem(functionName) {
|
|
1734
|
+
return new FunctionCallRegistryItem({
|
|
1735
|
+
functionName,
|
|
1736
|
+
handler: this.#missingFunctionCallHandler,
|
|
1737
|
+
cancelOnInterruption: true,
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
/** Return a terminal tool result when the LLM calls an unknown function. */
|
|
1741
|
+
async _missingFunctionCallHandler(params) {
|
|
1742
|
+
const cls = this.constructor;
|
|
1743
|
+
await params.resultCallback(cls.MISSING_FUNCTION_CALL_MESSAGE_TEMPLATE.replaceAll("{function_name}", params.functionName));
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Return the set of standard-tool names currently advertised to the LLM.
|
|
1747
|
+
*
|
|
1748
|
+
* Custom (LLM-specific) tools are not included, since they have no
|
|
1749
|
+
* consistent name field across adapters.
|
|
1750
|
+
*/
|
|
1751
|
+
static _advertisedToolNames(context) {
|
|
1752
|
+
const tools = context !== null && context !== undefined ? context.tools : null;
|
|
1753
|
+
if (tools === null || tools === undefined || !isGiven(tools)) {
|
|
1754
|
+
return new Set();
|
|
1755
|
+
}
|
|
1756
|
+
return new Set(tools.standardTools.map((t) => t.name));
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Log an appropriate message when a tool is called with no handler.
|
|
1760
|
+
*
|
|
1761
|
+
* Distinguishes two cases:
|
|
1762
|
+
*
|
|
1763
|
+
* - **Developer error:** the tool is advertised to the LLM but no handler
|
|
1764
|
+
* was wired up (a `FunctionSchema` must've been provided with neither
|
|
1765
|
+
* `handler` nor accompanying `registerFunction` call). Logged at error
|
|
1766
|
+
* level since this almost always indicates a bug.
|
|
1767
|
+
* - **Hallucination:** the tool is not in the currently advertised tool
|
|
1768
|
+
* set. Logged at warning level since this is model behavior the
|
|
1769
|
+
* application can do little about beyond returning a terminal result.
|
|
1770
|
+
*/
|
|
1771
|
+
_logMissingFunctionCall(functionName, context) {
|
|
1772
|
+
if (LLMService._advertisedToolNames(context).has(functionName)) {
|
|
1773
|
+
logger.error(`${this}: tool '${functionName}' is advertised to the LLM but has ` +
|
|
1774
|
+
"no handler — set FunctionSchema.handler (recommended) or call " +
|
|
1775
|
+
"register_function().");
|
|
1776
|
+
}
|
|
1777
|
+
else {
|
|
1778
|
+
logger.warning(`${this}: LLM called '${functionName}', which is not in the ` +
|
|
1779
|
+
"currently advertised tool set.");
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Whether the LLM may cancel calls of a registered tool.
|
|
1784
|
+
*
|
|
1785
|
+
* The one place that decides it, so what gets advertised and what a cancel
|
|
1786
|
+
* tool will actually stop can't drift apart.
|
|
1787
|
+
*
|
|
1788
|
+
* @param item The registered tool to judge.
|
|
1789
|
+
* @returns True if the tool opted in with `cancellableByLlm`.
|
|
1790
|
+
*/
|
|
1791
|
+
_isCancellable(item) {
|
|
1792
|
+
return item.cancellableByLlm;
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Names of the registered tools the LLM may cancel.
|
|
1796
|
+
*
|
|
1797
|
+
* @returns The tools that opted in with `cancellableByLlm: true`.
|
|
1798
|
+
*/
|
|
1799
|
+
_cancellableToolNames() {
|
|
1800
|
+
const names = new Set();
|
|
1801
|
+
for (const [name, item] of this._functions) {
|
|
1802
|
+
if (name && this._isCancellable(item))
|
|
1803
|
+
names.add(name);
|
|
1804
|
+
}
|
|
1805
|
+
return names;
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Match the advertised cancel tools to the tools currently registered.
|
|
1809
|
+
*
|
|
1810
|
+
* One cancel tool per cancellable tool, named for it, reconciled from a
|
|
1811
|
+
* single reading of the registry so the advertised set can never drift from
|
|
1812
|
+
* what is actually cancellable. A tool that never opted in has no cancel
|
|
1813
|
+
* tool at all, so there is nothing for the LLM to call against it.
|
|
1814
|
+
*/
|
|
1815
|
+
_syncCancelTools() {
|
|
1816
|
+
const wanted = new Map();
|
|
1817
|
+
for (const name of this._cancellableToolNames()) {
|
|
1818
|
+
wanted.set(cancelToolName(name), name);
|
|
1819
|
+
}
|
|
1820
|
+
for (const stale of [...this._cancelToolNames]) {
|
|
1821
|
+
if (wanted.has(stale))
|
|
1822
|
+
continue;
|
|
1823
|
+
delete this._adapter.builtinTools[stale];
|
|
1824
|
+
this._functions.delete(stale);
|
|
1825
|
+
this._cancelToolNames.delete(stale);
|
|
1826
|
+
}
|
|
1827
|
+
for (const [name, target] of wanted) {
|
|
1828
|
+
if (this._cancelToolNames.has(name)) {
|
|
1829
|
+
continue;
|
|
1830
|
+
}
|
|
1831
|
+
if (this._functions.has(name)) {
|
|
1832
|
+
// Once per name: this runs on every context frame, and the collision
|
|
1833
|
+
// persists for as long as both tools are registered.
|
|
1834
|
+
if (!this._warnedCancelCollisions.has(name)) {
|
|
1835
|
+
this._warnedCancelCollisions.add(name);
|
|
1836
|
+
logger.warning(`${this}: '${target}' is cancellable but its cancel tool can't be ` +
|
|
1837
|
+
`advertised — '${name}' is already registered as a tool of its own. ` +
|
|
1838
|
+
`Rename one of them, or the LLM has no way to stop ${target}.`);
|
|
1839
|
+
}
|
|
1840
|
+
continue;
|
|
1841
|
+
}
|
|
1842
|
+
this._adapter.builtinTools[name] = buildCancelToolSchema(target);
|
|
1843
|
+
this._functions.set(name, new FunctionCallRegistryItem({
|
|
1844
|
+
functionName: name,
|
|
1845
|
+
handler: this.#cancelToolHandler,
|
|
1846
|
+
cancelOnInterruption: true,
|
|
1847
|
+
}));
|
|
1848
|
+
this._cancelToolNames.add(name);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* Handle a `cancel_<tool>` invocation from the LLM.
|
|
1853
|
+
*
|
|
1854
|
+
* Which work to stop comes from the tool the model called, so the common
|
|
1855
|
+
* case takes no arguments at all. `tool_call_id` only has to be given to
|
|
1856
|
+
* choose between several running calls of the same tool.
|
|
1857
|
+
*
|
|
1858
|
+
* @param params Function call parameters, optionally carrying `tool_call_id`.
|
|
1859
|
+
*/
|
|
1860
|
+
async _cancelToolHandler(params) {
|
|
1861
|
+
const target = (params.functionName || "").slice(CANCEL_TOOL_PREFIX.length);
|
|
1862
|
+
const toolCallId = params.arguments.tool_call_id ?? null;
|
|
1863
|
+
const refuse = async (reason, extra = {}) => {
|
|
1864
|
+
logger.debug(`${this}: declining to cancel ${target} — ${reason}`);
|
|
1865
|
+
await params.resultCallback({ cancelled: null, reason, ...extra }, { properties: new FunctionCallResultProperties({ runLlm: true }) });
|
|
1866
|
+
};
|
|
1867
|
+
const candidates = [...this._functionCallTasks.values()].filter((item) => item.functionName === target && this._isCancellable(item.registryItem));
|
|
1868
|
+
if (candidates.length === 0) {
|
|
1869
|
+
await refuse(`no ${target} call is running`);
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
let match;
|
|
1873
|
+
if (toolCallId) {
|
|
1874
|
+
const found = candidates.find((i) => i.toolCallId === toolCallId);
|
|
1875
|
+
if (found === undefined) {
|
|
1876
|
+
await refuse(`no running ${target} call has that tool_call_id`);
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
match = found;
|
|
1880
|
+
}
|
|
1881
|
+
else if (candidates.length > 1) {
|
|
1882
|
+
// Several calls of the one tool: the model has to say which, so the
|
|
1883
|
+
// refusal carries the ids to say it with.
|
|
1884
|
+
// Spell the choices out in the reason itself: a model reading a refusal
|
|
1885
|
+
// acts on what it says far more readily than on a field beside it.
|
|
1886
|
+
const choices = candidates
|
|
1887
|
+
.map((i) => `${i.toolCallId} (called with ${JSON.stringify(i.arguments, (_k, v) => typeof v === "bigint" ? String(v) : v)})`)
|
|
1888
|
+
.join("; ");
|
|
1889
|
+
await refuse(`${candidates.length} ${target} calls are running: ${choices}. Call ` +
|
|
1890
|
+
`${params.functionName} again with tool_call_id set to the one to stop.`, {
|
|
1891
|
+
running: candidates.map((i) => ({ tool_call_id: i.toolCallId, arguments: i.arguments })),
|
|
1892
|
+
});
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
else {
|
|
1896
|
+
match = candidates[0];
|
|
1897
|
+
}
|
|
1898
|
+
logger.debug(`${this}: cancelling [${match.functionName}:${match.toolCallId}]`);
|
|
1899
|
+
await this._cancelFunctionCallsByToolCallId(match.toolCallId);
|
|
1900
|
+
await params.resultCallback({ cancelled: match.toolCallId, function_name: match.functionName }, { properties: new FunctionCallResultProperties({ runLlm: true }) });
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Cancel the in-flight function calls selected by a predicate.
|
|
1904
|
+
*
|
|
1905
|
+
* Cancelling a call cancels its handler's task (delivering a
|
|
1906
|
+
* `CancelledError`) so cancel-aware handlers run their cleanup, broadcasts a
|
|
1907
|
+
* `FunctionCallCancelFrame` so the rest of the pipeline can settle the
|
|
1908
|
+
* call, and notifies application code via `on_function_calls_cancelled`.
|
|
1909
|
+
*
|
|
1910
|
+
* @param predicate Selects which in-flight calls to cancel.
|
|
1911
|
+
* @param options.reason Short description of what triggered the
|
|
1912
|
+
* cancellation, included in the log messages.
|
|
1913
|
+
* @param options.runLlm Whether inference should run once the cancelled
|
|
1914
|
+
* calls are settled in the context.
|
|
1915
|
+
* @returns The function calls that were cancelled.
|
|
1916
|
+
*/
|
|
1917
|
+
async _cancelFunctionCallTasks(predicate, options) {
|
|
1918
|
+
const runLlm = options.runLlm ?? false;
|
|
1919
|
+
const cancelledTasks = new Set();
|
|
1920
|
+
const cancelledItems = [];
|
|
1921
|
+
// Snapshot first: cancelTask awaits, during which done callbacks may
|
|
1922
|
+
// mutate _functionCallTasks.
|
|
1923
|
+
for (const [task, runnerItem] of [...this._functionCallTasks.entries()]) {
|
|
1924
|
+
if (!predicate(runnerItem)) {
|
|
1925
|
+
continue;
|
|
1926
|
+
}
|
|
1927
|
+
const name = runnerItem.functionName;
|
|
1928
|
+
const toolCallId = runnerItem.toolCallId;
|
|
1929
|
+
logger.debug(`${this} Cancelling function call [${name}:${toolCallId}] (${options.reason})...`);
|
|
1930
|
+
// Settle before cancelling: a handler that catches its CancelledError
|
|
1931
|
+
// and reports a result while unwinding must not be able to reopen a
|
|
1932
|
+
// call the pipeline has stopped tracking.
|
|
1933
|
+
runnerItem.settled = true;
|
|
1934
|
+
if (task) {
|
|
1935
|
+
// We remove the callback because we are going to cancel the task
|
|
1936
|
+
// next, otherwise we will be removing it from the map while we are
|
|
1937
|
+
// iterating.
|
|
1938
|
+
task.removeDoneCallback(this.#functionCallTaskDone);
|
|
1939
|
+
await this.cancelTask(task);
|
|
1940
|
+
cancelledTasks.add(task);
|
|
1941
|
+
}
|
|
1942
|
+
cancelledItems.push(await this._broadcastFunctionCallCancelled(runnerItem, { runLlm }));
|
|
1943
|
+
logger.debug(`${this} Function call [${name}:${toolCallId}] has been cancelled`);
|
|
1944
|
+
}
|
|
1945
|
+
// Remove all cancelled tasks from our map.
|
|
1946
|
+
for (const task of cancelledTasks) {
|
|
1947
|
+
this._functionCallTaskFinished(task);
|
|
1948
|
+
}
|
|
1949
|
+
if (cancelledItems.length > 0) {
|
|
1950
|
+
await this._callEventHandler("on_function_calls_cancelled", cancelledItems);
|
|
1951
|
+
}
|
|
1952
|
+
return cancelledItems;
|
|
1953
|
+
}
|
|
1954
|
+
/**
|
|
1955
|
+
* Settle a cancelled function call downstream.
|
|
1956
|
+
*
|
|
1957
|
+
* @param runnerItem The function call that was cancelled.
|
|
1958
|
+
* @param options.runLlm Whether inference should run once the call is
|
|
1959
|
+
* settled in the context.
|
|
1960
|
+
* @returns The cancelled call, described for `on_function_calls_cancelled`.
|
|
1961
|
+
*/
|
|
1962
|
+
async _broadcastFunctionCallCancelled(runnerItem, options = {}) {
|
|
1963
|
+
await this.broadcastFrame(FunctionCallCancelFrame, {
|
|
1964
|
+
functionName: runnerItem.functionName,
|
|
1965
|
+
toolCallId: runnerItem.toolCallId,
|
|
1966
|
+
runLlm: options.runLlm ?? false,
|
|
1967
|
+
});
|
|
1968
|
+
return new FunctionCallFromLLM({
|
|
1969
|
+
functionName: runnerItem.functionName,
|
|
1970
|
+
toolCallId: runnerItem.toolCallId,
|
|
1971
|
+
arguments: runnerItem.arguments,
|
|
1972
|
+
context: runnerItem.context,
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
/**
|
|
1976
|
+
* Cancel a function call that ran past its deadline.
|
|
1977
|
+
*
|
|
1978
|
+
* The handler usually still owns the call and is cancelled outright. If it
|
|
1979
|
+
* happened to finish while the timeout was being processed its task is
|
|
1980
|
+
* already gone and there is nothing left to cancel, but the deadline has
|
|
1981
|
+
* settled the call, so its result is rejected — the pipeline is told the
|
|
1982
|
+
* call is over regardless, or it waits on a result that can never arrive.
|
|
1983
|
+
*
|
|
1984
|
+
* A deadline is the one cancellation nothing else follows up on, so it asks
|
|
1985
|
+
* for inference: without it the LLM never learns the call is over and the
|
|
1986
|
+
* user is left waiting on an answer that never comes.
|
|
1987
|
+
*
|
|
1988
|
+
* @param runnerItem The function call that timed out.
|
|
1989
|
+
*/
|
|
1990
|
+
async _timeoutFunctionCall(runnerItem) {
|
|
1991
|
+
const cancelled = await this._cancelFunctionCallTasks((item) => item.toolCallId === runnerItem.toolCallId, { reason: "timeout", runLlm: true });
|
|
1992
|
+
if (cancelled.length > 0) {
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
const item = await this._broadcastFunctionCallCancelled(runnerItem, { runLlm: true });
|
|
1996
|
+
await this._callEventHandler("on_function_calls_cancelled", [item]);
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Cancel every function call in flight, whether or not it survives
|
|
2000
|
+
* interruptions.
|
|
2001
|
+
*
|
|
2002
|
+
* Each handler is cancelled, the call is settled downstream as cancelled,
|
|
2003
|
+
* and `on_function_calls_cancelled` fires. No inference runs for the
|
|
2004
|
+
* cancellations.
|
|
2005
|
+
*
|
|
2006
|
+
* @param options.reason What triggered the cancellation, for the logs.
|
|
2007
|
+
* @returns The calls that were cancelled.
|
|
2008
|
+
*/
|
|
2009
|
+
async cancelFunctionCalls(options = {}) {
|
|
2010
|
+
return await this._cancelFunctionCallTasks(() => true, {
|
|
2011
|
+
reason: options.reason ?? "cancelled",
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
/**
|
|
2015
|
+
* Cancel in-progress function call tasks by their toolCallId.
|
|
2016
|
+
*
|
|
2017
|
+
* @param toolCallId toolCallId to cancel.
|
|
2018
|
+
*/
|
|
2019
|
+
async _cancelFunctionCallsByToolCallId(toolCallId) {
|
|
2020
|
+
await this._cancelFunctionCallTasks((item) => item.toolCallId === toolCallId, {
|
|
2021
|
+
reason: "User request",
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
async _cancelFunctionCall(functionName) {
|
|
2025
|
+
await this._cancelFunctionCallTasks((item) => item.registryItem.functionName === functionName, {
|
|
2026
|
+
reason: "interruption",
|
|
2027
|
+
});
|
|
2028
|
+
}
|
|
2029
|
+
_functionCallTaskFinished(task) {
|
|
2030
|
+
this._functionCallTasks.delete(task);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
// ---------------------------------------------------------------------------
|
|
2034
|
+
// WebSocket LLM service base
|
|
2035
|
+
// ---------------------------------------------------------------------------
|
|
2036
|
+
/**
|
|
2037
|
+
* Raised by `_wsSend`/`_wsRecv` after a transparent reconnection.
|
|
2038
|
+
*
|
|
2039
|
+
* Signals that the WebSocket connection was lost and automatically
|
|
2040
|
+
* re-established. The current inference should be restarted — any
|
|
2041
|
+
* connection-local state on the server (e.g. cached responses) is gone.
|
|
2042
|
+
*/
|
|
2043
|
+
export class WebsocketReconnectedError extends Error {
|
|
2044
|
+
name = "WebsocketReconnectedError";
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* Base class for websocket-based LLM services.
|
|
2048
|
+
*
|
|
2049
|
+
* Each LLM inference is a discrete request/response exchange: send one
|
|
2050
|
+
* request, receive events inline until a terminal event, then wait for the
|
|
2051
|
+
* next frame to trigger an inference. This contrasts with
|
|
2052
|
+
* `WebsocketTTSService` / `WebsocketSTTService` which stream data
|
|
2053
|
+
* continuously via a background receive loop (`_receiveTaskHandler`). This
|
|
2054
|
+
* class does **not** start a background receive loop.
|
|
2055
|
+
*
|
|
2056
|
+
* Provides connection lifecycle management (connect on setup, disconnect on
|
|
2057
|
+
* stop/cancel), automatic reconnection with exponential backoff, and three
|
|
2058
|
+
* helpers for running each inference:
|
|
2059
|
+
*
|
|
2060
|
+
* 1. `_ensureConnected()` — verify the websocket is alive, reconnect with
|
|
2061
|
+
* exponential backoff if not.
|
|
2062
|
+
* 2. `_wsSend(message)` — send the inference request as JSON.
|
|
2063
|
+
* 3. `_wsRecv()` — receive and parse response events one at a time until the
|
|
2064
|
+
* caller sees a terminal event.
|
|
2065
|
+
*
|
|
2066
|
+
* `_wsSend` and `_wsRecv` catch `ConnectionClosed` transparently,
|
|
2067
|
+
* auto-reconnect via `_tryReconnect`, and raise `WebsocketReconnectedError`
|
|
2068
|
+
* so callers know the inference must be restarted. If reconnection fails,
|
|
2069
|
+
* the original `ConnectionClosed` propagates.
|
|
2070
|
+
*
|
|
2071
|
+
* Subclasses must implement:
|
|
2072
|
+
* - `_connectWebsocket()`: establish the websocket connection.
|
|
2073
|
+
* - `_disconnectWebsocket()`: close the websocket and clean up.
|
|
2074
|
+
*
|
|
2075
|
+
* Event handlers:
|
|
2076
|
+
* - on_connection_error: called when a websocket connection error occurs.
|
|
2077
|
+
*
|
|
2078
|
+
* @example
|
|
2079
|
+
* llm.eventHandler("on_connection_error", async (llm: LLMService, error: string) => {
|
|
2080
|
+
* logger.error(`LLM connection error: ${error}`);
|
|
2081
|
+
* });
|
|
2082
|
+
*/
|
|
2083
|
+
export class WebsocketLLMService extends WebsocketService(LLMService) {
|
|
2084
|
+
constructor(options = {}) {
|
|
2085
|
+
super(options);
|
|
2086
|
+
this._registerEventHandler("on_connection_error");
|
|
2087
|
+
}
|
|
2088
|
+
// -- lifecycle ------------------------------------------------------------
|
|
2089
|
+
/** Connect: reset flags and establish the websocket. */
|
|
2090
|
+
async _connect() {
|
|
2091
|
+
await super._connect();
|
|
2092
|
+
await this._connectWebsocket();
|
|
2093
|
+
}
|
|
2094
|
+
/** Disconnect: set flags and close the websocket. */
|
|
2095
|
+
async _disconnect() {
|
|
2096
|
+
await super._disconnect();
|
|
2097
|
+
await this._disconnectWebsocket();
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Set up the service and connect.
|
|
2101
|
+
*
|
|
2102
|
+
* @param setup Configuration object containing setup parameters.
|
|
2103
|
+
*/
|
|
2104
|
+
async setup(setup) {
|
|
2105
|
+
await super.setup(setup);
|
|
2106
|
+
await this._connect();
|
|
2107
|
+
}
|
|
2108
|
+
/**
|
|
2109
|
+
* Stop the service and close WebSocket connection.
|
|
2110
|
+
*
|
|
2111
|
+
* @param frame The end frame triggering service shutdown.
|
|
2112
|
+
*/
|
|
2113
|
+
async stop(frame) {
|
|
2114
|
+
await super.stop(frame);
|
|
2115
|
+
await this._disconnect();
|
|
2116
|
+
}
|
|
2117
|
+
/**
|
|
2118
|
+
* Cancel the service and close WebSocket connection.
|
|
2119
|
+
*
|
|
2120
|
+
* @param frame The cancel frame triggering service cancellation.
|
|
2121
|
+
*/
|
|
2122
|
+
async cancel(frame) {
|
|
2123
|
+
await super.cancel(frame);
|
|
2124
|
+
await this._disconnect();
|
|
2125
|
+
}
|
|
2126
|
+
// -- per-inference helpers ------------------------------------------------
|
|
2127
|
+
/**
|
|
2128
|
+
* Send a JSON message over the websocket.
|
|
2129
|
+
*
|
|
2130
|
+
* Guards against sends during intentional disconnect. If the send fails
|
|
2131
|
+
* with `ConnectionClosed`, attempts to reconnect and raises
|
|
2132
|
+
* `WebsocketReconnectedError` on success so the caller can restart the
|
|
2133
|
+
* inference. If reconnection fails, the original `ConnectionClosed`
|
|
2134
|
+
* propagates.
|
|
2135
|
+
*
|
|
2136
|
+
* @param message The message dict to serialize and send.
|
|
2137
|
+
*/
|
|
2138
|
+
async _wsSend(message) {
|
|
2139
|
+
if (this._disconnecting || !this._websocket) {
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
try {
|
|
2143
|
+
await this._websocket.send(JSON.stringify(message));
|
|
2144
|
+
}
|
|
2145
|
+
catch (e) {
|
|
2146
|
+
if (!(e instanceof ConnectionClosed))
|
|
2147
|
+
throw e;
|
|
2148
|
+
if (this._disconnecting) {
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
const success = await this._tryReconnect({
|
|
2152
|
+
reportError: (error, options) => this._reportError(error, options),
|
|
2153
|
+
});
|
|
2154
|
+
if (success) {
|
|
2155
|
+
throw new WebsocketReconnectedError();
|
|
2156
|
+
}
|
|
2157
|
+
throw e;
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* Receive and parse a JSON message from the websocket.
|
|
2162
|
+
*
|
|
2163
|
+
* If the receive fails with `ConnectionClosed`, attempts to reconnect and
|
|
2164
|
+
* raises `WebsocketReconnectedError` on success. If reconnection fails, the
|
|
2165
|
+
* original `ConnectionClosed` propagates.
|
|
2166
|
+
*
|
|
2167
|
+
* @returns The parsed JSON message as a dict.
|
|
2168
|
+
*/
|
|
2169
|
+
async _wsRecv() {
|
|
2170
|
+
// Should never happen — `_ensureConnected` (which callers must invoke
|
|
2171
|
+
// first) raises if it can't establish a websocket. Match that contract
|
|
2172
|
+
// here.
|
|
2173
|
+
if (this._websocket === null) {
|
|
2174
|
+
throw new Error(`${this} _ws_recv called without a websocket`);
|
|
2175
|
+
}
|
|
2176
|
+
try {
|
|
2177
|
+
const raw = await this._websocket.recv();
|
|
2178
|
+
return JSON.parse(typeof raw === "string" ? raw : new TextDecoder().decode(raw));
|
|
2179
|
+
}
|
|
2180
|
+
catch (e) {
|
|
2181
|
+
if (!(e instanceof ConnectionClosed))
|
|
2182
|
+
throw e;
|
|
2183
|
+
if (this._disconnecting) {
|
|
2184
|
+
throw e;
|
|
2185
|
+
}
|
|
2186
|
+
const success = await this._tryReconnect({
|
|
2187
|
+
reportError: (error, options) => this._reportError(error, options),
|
|
2188
|
+
});
|
|
2189
|
+
if (success) {
|
|
2190
|
+
throw new WebsocketReconnectedError();
|
|
2191
|
+
}
|
|
2192
|
+
throw e;
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
/**
|
|
2196
|
+
* Ensure the websocket is connected, reconnecting if needed.
|
|
2197
|
+
*
|
|
2198
|
+
* Uses `_tryReconnect` with exponential backoff.
|
|
2199
|
+
*
|
|
2200
|
+
* @throws Error if the connection could not be established.
|
|
2201
|
+
*/
|
|
2202
|
+
async _ensureConnected() {
|
|
2203
|
+
if (this._websocket && this._websocket.state !== State.CLOSED) {
|
|
2204
|
+
return;
|
|
2205
|
+
}
|
|
2206
|
+
const success = await this._tryReconnect({
|
|
2207
|
+
reportError: (error, options) => this._reportError(error, options),
|
|
2208
|
+
});
|
|
2209
|
+
if (!success) {
|
|
2210
|
+
throw new Error(`${this} failed to establish WebSocket connection`);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
// -- WebsocketService interface -------------------------------------------
|
|
2214
|
+
/**
|
|
2215
|
+
* Not used — messages are received inline during each inference.
|
|
2216
|
+
*
|
|
2217
|
+
* This satisfies the `WebsocketService` abstract method but is never called
|
|
2218
|
+
* because `_receiveTaskHandler` is never started.
|
|
2219
|
+
*/
|
|
2220
|
+
async _receiveMessages() {
|
|
2221
|
+
throw new NotImplementedError("WebsocketLLMService receives messages inline during inference, " +
|
|
2222
|
+
"not via a continuous background loop");
|
|
2223
|
+
}
|
|
2224
|
+
async _reportError(error, options = {}) {
|
|
2225
|
+
await this._callEventHandler("on_connection_error", error.error);
|
|
2226
|
+
await this.pushErrorFrame(error, {
|
|
2227
|
+
forceTreatAsPermanent: options.forceTreatAsPermanent ?? false,
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
//# sourceMappingURL=llm_service.js.map
|