From f368fe51346ed325998aebdcf8e20560cfc76d99 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 2 Apr 2026 14:07:03 +0530 Subject: [PATCH] feat: set calculator as custom tool on demand --- ...b647d354_add_calculator_in_toolcategory.py | 61 ++++++++++++ api/enums.py | 1 + api/routes/tool.py | 14 ++- api/services/configuration/registry.py | 4 +- api/services/workflow/pipecat_engine.py | 96 +------------------ .../pipecat_engine_context_composer.py | 12 +-- .../workflow/pipecat_engine_custom_tools.py | 40 ++++++++ .../components/BuiltinToolConfig.tsx | 60 ++++++++++++ .../app/tools/[toolUuid]/components/index.ts | 1 + ui/src/app/tools/[toolUuid]/page.tsx | 32 ++++++- ui/src/app/tools/config.tsx | 77 +++++++-------- ui/src/app/tools/page.tsx | 2 + ui/src/client/types.gen.ts | 22 ++++- 13 files changed, 265 insertions(+), 157 deletions(-) create mode 100644 api/alembic/versions/c71db647d354_add_calculator_in_toolcategory.py create mode 100644 ui/src/app/tools/[toolUuid]/components/BuiltinToolConfig.tsx diff --git a/api/alembic/versions/c71db647d354_add_calculator_in_toolcategory.py b/api/alembic/versions/c71db647d354_add_calculator_in_toolcategory.py new file mode 100644 index 0000000..236d12b --- /dev/null +++ b/api/alembic/versions/c71db647d354_add_calculator_in_toolcategory.py @@ -0,0 +1,61 @@ +"""add calculator in ToolCategory + +Revision ID: c71db647d354 +Revises: b3a1c7e94f12 +Create Date: 2026-04-02 13:53:46.184244 + +""" + +from typing import Sequence, Union + +from alembic import op +from alembic_postgresql_enum import TableReference + +# revision identifiers, used by Alembic. +revision: str = "c71db647d354" +down_revision: Union[str, None] = "b3a1c7e94f12" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("ix_api_keys_key_hash"), table_name="api_keys") + op.create_index("ix_api_keys_key_hash", "api_keys", ["key_hash"], unique=False) + op.sync_enum_values( + enum_schema="public", + enum_name="tool_category", + new_values=[ + "http_api", + "end_call", + "transfer_call", + "calculator", + "native", + "integration", + ], + affected_columns=[ + TableReference( + table_schema="public", table_name="tools", column_name="category" + ) + ], + enum_values_to_rename=[], + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.sync_enum_values( + enum_schema="public", + enum_name="tool_category", + new_values=["http_api", "end_call", "transfer_call", "native", "integration"], + affected_columns=[ + TableReference( + table_schema="public", table_name="tools", column_name="category" + ) + ], + enum_values_to_rename=[], + ) + op.drop_index("ix_api_keys_key_hash", table_name="api_keys") + op.create_index(op.f("ix_api_keys_key_hash"), "api_keys", ["key_hash"], unique=True) + # ### end Alembic commands ### diff --git a/api/enums.py b/api/enums.py index bd3425b..85b6829 100644 --- a/api/enums.py +++ b/api/enums.py @@ -128,6 +128,7 @@ class ToolCategory(Enum): HTTP_API = "http_api" # Custom HTTP API calls (implemented) END_CALL = "end_call" # End call tool TRANSFER_CALL = "transfer_call" # Transfer call to phone number (Twilio only) + CALCULATOR = "calculator" # Built-in calculator tool NATIVE = "native" # Built-in integrations (future: dtmf_input) INTEGRATION = "integration" # Third-party integrations (future: Google Calendar, Salesforce, etc.) diff --git a/api/routes/tool.py b/api/routes/tool.py index ba8e708..a0eeffd 100644 --- a/api/routes/tool.py +++ b/api/routes/tool.py @@ -134,9 +134,21 @@ class TransferCallToolDefinition(BaseModel): config: TransferCallConfig = Field(description="Transfer Call configuration") +class CalculatorToolDefinition(BaseModel): + """Tool definition for Calculator tools (no configuration needed).""" + + schema_version: int = Field(default=1, description="Schema version") + type: Literal["calculator"] = Field(description="Tool type") + + # Union type for tool definitions - Pydantic will discriminate based on 'type' field ToolDefinition = Annotated[ - Union[HttpApiToolDefinition, EndCallToolDefinition, TransferCallToolDefinition], + Union[ + HttpApiToolDefinition, + EndCallToolDefinition, + TransferCallToolDefinition, + CalculatorToolDefinition, + ], Field(discriminator="type"), ] diff --git a/api/services/configuration/registry.py b/api/services/configuration/registry.py index 51939b7..e218c3c 100644 --- a/api/services/configuration/registry.py +++ b/api/services/configuration/registry.py @@ -318,9 +318,7 @@ OPENAI_REALTIME_VOICES = [ GOOGLE_REALTIME_MODELS = ["gemini-3.1-flash-live-preview"] GOOGLE_REALTIME_VOICES = ["Puck", "Charon", "Kore", "Fenrir", "Aoede"] -GOOGLE_REALTIME_LANGUAGES = [ - "en" -] +GOOGLE_REALTIME_LANGUAGES = ["en"] @register_service(ServiceType.REALTIME) diff --git a/api/services/workflow/pipecat_engine.py b/api/services/workflow/pipecat_engine.py index 6ad2000..7b3801e 100644 --- a/api/services/workflow/pipecat_engine.py +++ b/api/services/workflow/pipecat_engine.py @@ -40,20 +40,14 @@ from api.services.workflow.pipecat_engine_context_composer import ( ) from api.services.workflow.pipecat_engine_custom_tools import ( CustomToolManager, - get_function_schema, ) from api.services.workflow.pipecat_engine_variable_extractor import ( VariableExtractionManager, ) -from api.services.workflow.tools.calculator import get_calculator_tools, safe_calculator from api.services.workflow.tools.knowledge_base import ( retrieve_from_knowledge_base, ) -from api.services.workflow.tools.timezone import ( - convert_time, - get_current_time, - get_time_tools, -) +from api.services.workflow.tools.timezone import get_current_time from api.utils.template_renderer import render_template @@ -93,9 +87,6 @@ class PipecatEngine: # access to _context self._variable_extraction_manager = None - # Lazy loaded built-in function schemas - self._builtin_function_schemas: Optional[list[dict]] = None - # Track current LLM reference text for TTS aggregation correction self._current_llm_generation_reference_text: str = "" @@ -144,36 +135,6 @@ class PipecatEngine: return None return tracing_ctx.get_turn_context() or tracing_ctx.get_conversation_context() - @property - def builtin_function_schemas(self) -> list[dict]: - """Get built-in function schemas (calculator and timezone tools).""" - if self._builtin_function_schemas is None: - self._builtin_function_schemas = [] - - # Transform calculator tools to get_function_schema format - for tool in get_calculator_tools(): - func = tool["function"] - schema = get_function_schema( - func["name"], - func["description"], - properties=func["parameters"]["properties"], - required=func["parameters"]["required"], - ) - self._builtin_function_schemas.append(schema) - - # Transform timezone tools to get_function_schema format - for tool in get_time_tools(): - func = tool["function"] - schema = get_function_schema( - func["name"], - func["description"], - properties=func["parameters"]["properties"], - required=func["parameters"]["required"], - ) - self._builtin_function_schemas.append(schema) - - return self._builtin_function_schemas - async def initialize(self): # TODO: May be set_node in a separate task so that we return from initialize immediately if self._initialized: @@ -197,9 +158,6 @@ class PipecatEngine: except Exception as e: logger.error(f"Failed to fetch current EST time: {e}") - # Register built-in functions with the LLM - await self._register_builtin_functions() - await self.set_node(self.workflow.start_node_id) logger.debug(f"{self.__class__.__name__} initialized") @@ -316,57 +274,6 @@ class PipecatEngine: cancel_on_interruption=False, ) - async def _register_builtin_functions(self): - """Register built-in functions (calculator and timezone) with the LLM.""" - logger.debug("Registering built-in functions with LLM") - - # Register calculator function - async def calculate_func(function_call_params: FunctionCallParams) -> None: - logger.info(f"LLM Function Call EXECUTED: safe_calculator") - logger.info(f"Arguments: {function_call_params.arguments}") - - try: - expr = function_call_params.arguments.get("expression", "") - result = safe_calculator(expr) - await function_call_params.result_callback( - {"expression": expr, "result": result} - ) - except Exception as e: - await function_call_params.result_callback({"error": str(e)}) - - # Register timezone functions - async def get_current_time_func( - function_call_params: FunctionCallParams, - ) -> None: - logger.info(f"LLM Function Call EXECUTED: get_current_time") - logger.info(f"Arguments: {function_call_params.arguments}") - - try: - timezone = function_call_params.arguments.get("timezone", "UTC") - result = get_current_time(timezone) - await function_call_params.result_callback(result) - except Exception as e: - await function_call_params.result_callback({"error": str(e)}) - - async def convert_time_func(function_call_params: FunctionCallParams) -> None: - logger.info(f"LLM Function Call EXECUTED: convert_time") - logger.info(f"Arguments: {function_call_params.arguments}") - - try: - result = convert_time( - function_call_params.arguments.get("source_timezone"), - function_call_params.arguments.get("time"), - function_call_params.arguments.get("target_timezone"), - ) - await function_call_params.result_callback(result) - except Exception as e: - await function_call_params.result_callback({"error": str(e)}) - - # Register all built-in functions - self.llm.register_function("safe_calculator", calculate_func) - self.llm.register_function("get_current_time", get_current_time_func) - self.llm.register_function("convert_time", convert_time_func) - async def _register_knowledge_base_function( self, document_uuids: list[str] ) -> None: @@ -553,7 +460,6 @@ class PipecatEngine: ) functions = await compose_functions_for_node( node=node, - builtin_function_schemas=self.builtin_function_schemas, custom_tool_manager=self._custom_tool_manager, ) await self._update_llm_context(system_prompt, functions) diff --git a/api/services/workflow/pipecat_engine_context_composer.py b/api/services/workflow/pipecat_engine_context_composer.py index 0613cd7..0f85c64 100644 --- a/api/services/workflow/pipecat_engine_context_composer.py +++ b/api/services/workflow/pipecat_engine_context_composer.py @@ -86,27 +86,23 @@ def compose_system_prompt_for_node( async def compose_functions_for_node( *, node: "Node", - builtin_function_schemas: list[dict], custom_tool_manager: Optional["CustomToolManager"], ) -> list[dict]: """Compose the function/tool schemas for a workflow node. - Gathers built-in tools, knowledge-base tools, custom tools, - and transition function schemas into a single list. + Gathers knowledge-base tools, custom tools (including built-in + categories like calculator), and transition function schemas + into a single list. Args: node: The workflow node to compose functions for. - builtin_function_schemas: Pre-computed schemas for built-in tools. - custom_tool_manager: Manager for user-defined custom tools (may be None). + custom_tool_manager: Manager for custom and built-in tools (may be None). Returns: A list of function schemas to register with the LLM. """ functions: list[dict] = [] - # Built-in tools (calculator, timezone) - functions.extend(builtin_function_schemas) - # Knowledge base retrieval tool if node.document_uuids: kb_tool_def = get_knowledge_base_tool(node.document_uuids) diff --git a/api/services/workflow/pipecat_engine_custom_tools.py b/api/services/workflow/pipecat_engine_custom_tools.py index 77c2ea5..b0dd1c0 100644 --- a/api/services/workflow/pipecat_engine_custom_tools.py +++ b/api/services/workflow/pipecat_engine_custom_tools.py @@ -23,6 +23,7 @@ from api.services.telephony.transfer_event_protocol import TransferContext from api.services.workflow.disposition_mapper import ( get_organization_id_from_workflow_run, ) +from api.services.workflow.tools.calculator import get_calculator_tools, safe_calculator from api.services.workflow.tools.custom_tool import ( execute_http_tool, tool_to_function_schema, @@ -105,6 +106,20 @@ class CustomToolManager: schemas: list[FunctionSchema] = [] for tool in tools: + if tool.category == ToolCategory.CALCULATOR.value: + # Built-in calculator: return pre-defined schemas + for tool_def in get_calculator_tools(): + func = tool_def["function"] + schemas.append( + get_function_schema( + func["name"], + func["description"], + properties=func["parameters"]["properties"], + required=func["parameters"]["required"], + ) + ) + continue + raw_schema = tool_to_function_schema(tool) function_name = raw_schema["function"]["name"] @@ -146,6 +161,14 @@ class CustomToolManager: tools = await db_client.get_tools_by_uuids(tool_uuids, organization_id) for tool in tools: + if tool.category == ToolCategory.CALCULATOR.value: + self._register_calculator_handler() + logger.debug( + f"Registered calculator tool handler " + f"(tool_uuid: {tool.tool_uuid})" + ) + continue + schema = tool_to_function_schema(tool) function_name = schema["function"]["name"] @@ -193,6 +216,23 @@ class CustomToolManager: return handler, timeout_secs, cancel_on_interruption + def _register_calculator_handler(self) -> None: + """Register the built-in calculator function with the LLM.""" + + async def calculate_func(function_call_params: FunctionCallParams) -> None: + logger.info("LLM Function Call EXECUTED: safe_calculator") + logger.info(f"Arguments: {function_call_params.arguments}") + try: + expr = function_call_params.arguments.get("expression", "") + result = safe_calculator(expr) + await function_call_params.result_callback( + {"expression": expr, "result": result} + ) + except Exception as e: + await function_call_params.result_callback({"error": str(e)}) + + self._engine.llm.register_function("safe_calculator", calculate_func) + def _create_http_tool_handler(self, tool: Any, function_name: str): """Create a handler function for an HTTP API tool. diff --git a/ui/src/app/tools/[toolUuid]/components/BuiltinToolConfig.tsx b/ui/src/app/tools/[toolUuid]/components/BuiltinToolConfig.tsx new file mode 100644 index 0000000..5968237 --- /dev/null +++ b/ui/src/app/tools/[toolUuid]/components/BuiltinToolConfig.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; + +export interface BuiltinToolConfigProps { + name: string; + onNameChange: (name: string) => void; + description: string; + onDescriptionChange: (description: string) => void; + title: string; + subtitle: string; +} + +export function BuiltinToolConfig({ + name, + onNameChange, + description, + onDescriptionChange, + title, + subtitle, +}: BuiltinToolConfigProps) { + return ( + + + {title} + {subtitle} + + + {/* Tool Name */} +
+ + onNameChange(e.target.value)} + placeholder="Tool name" + /> +
+ + {/* Tool Description */} +
+ +

+ Provide a description which makes it easy for LLM to understand what this tool does +

+