Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add system tool calls to the chat endpoint #1179

Draft
wants to merge 30 commits into
base: dev
Choose a base branch
from

Conversation

whiterabbit1983
Copy link
Contributor

@whiterabbit1983 whiterabbit1983 commented Feb 20, 2025

User description

Closes #1285


EntelligenceAI PR Summary

The recent update introduces a new utility function, eval_tool_calls, in tools.py to enhance the evaluation of tool calls within chat sessions. The chat function in chat.py has been updated to utilize this utility, replacing direct calls to litellm.acompletion. This change aims to improve code modularity and streamline the integration of tool responses in chat sessions.


PR Type

Enhancement, Tests


Description

  • Introduced tool_calls_evaluator to streamline tool call handling in chat sessions.

  • Added a new utility function call_tool for executing system tool calls.

  • Implemented extensive unit tests for tool_calls_evaluator and call_tool.

  • Enhanced modularity and maintainability of the chat endpoint.


Changes walkthrough 📝

Relevant files
Enhancement
chat.py
Refactor chat endpoint to use tool evaluator                         

agents-api/agents_api/routers/sessions/chat.py

  • Integrated tool_calls_evaluator into the chat function.
  • Replaced direct litellm.acompletion calls with evaluated tool calls.
  • Enhanced modularity by delegating tool call handling.
  • +7/-1     
    tools.py
    Introduce tool call evaluation utilities                                 

    agents-api/agents_api/routers/utils/tools.py

  • Added tool_calls_evaluator decorator for tool call evaluation.
  • Implemented call_tool function to handle system tool calls.
  • Defined _system_tool_handlers mapping for tool-specific logic.
  • Added utility functions for search request creation and tool handling.
  • +268/-0 
    Tests
    test_tool_calls_evaluator.py
    Add unit tests for tool evaluation utilities                         

    agents-api/tests/test_tool_calls_evaluator.py

  • Added unit tests for call_tool covering various tool operations.
  • Tested tool_calls_evaluator for correct tool call handling.
  • Included edge cases like unknown tools and non-matching tool types.
  • Verified iterative tool call evaluation until completion.
  • +524/-0 

    Need help?
  • Type /help how to ... in the comments thread for any questions about Qodo Merge usage.
  • Check out the documentation for more information.

  • Important

    Refactor chat endpoint to use tool_calls_evaluator for tool call handling, enhancing modularity and adding extensive tests.

    • Behavior:
      • Refactor chat() in chat.py to use tool_calls_evaluator for handling tool calls, replacing direct litellm.acompletion calls.
      • Introduce call_tool in tools.py for executing system tool calls.
    • Utilities:
      • Add tool_calls_evaluator decorator in tools.py for tool call evaluation.
      • Define _system_tool_handlers mapping in tools.py for tool-specific logic.
    • Tests:
      • Add test_tool_calls_evaluator.py with unit tests for tool_calls_evaluator and call_tool.
      • Cover various tool operations and edge cases like unknown tools.
    • Misc:
      • Add utility functions for search request creation in tools.py.

    This description was created by Ellipsis for ebd3769. It will automatically update as commits are pushed.

    Copy link
    Contributor

    Walkthrough

    This PR introduces a new utility function, eval_tool_calls, in tools.py to enhance tool call evaluations within chat sessions. The chat function in chat.py is modified to leverage this utility, replacing direct calls to litellm.acompletion. These changes aim to streamline tool call handling, improve code modularity, and enhance the integration of tool responses into chat sessions.

    Changes

    File(s) Summary
    agents-api/agents_api/routers/sessions/chat.py Updated chat function to integrate eval_tool_calls utility, replacing direct litellm.acompletion calls for dynamic tool call processing.
    agents-api/agents_api/routers/utils/tools.py Added eval_tool_calls function to manage tool call evaluations, supporting operations like create, update, and search for agents, users, sessions, and tasks.
    Entelligence.ai can learn from your feedback. Simply add 👍 / 👎 emojis to teach it your preferences. More shortcuts below

    Emoji Descriptions:

    • ⚠️ Potential Issue - May require further investigation.
    • 🔒 Security Vulnerability - Fix to ensure system safety.
    • 💻 Code Improvement - Suggestions to enhance code quality.
    • 🔨 Refactor Suggestion - Recommendations for restructuring code.
    • ℹ️ Others - General comments and information.

    Interact with the Bot:

    • Send a message or request using the format:
      @bot + *your message*
    Example: @bot Can you suggest improvements for this code?
    
    • Help the Bot learn by providing feedback on its responses.
      @bot + *feedback*
    Example: @bot Do not comment on `save_auth` function !
    

    Copy link
    Contributor

    qodo-merge-pro-for-open-source bot commented Feb 20, 2025

    CI Feedback 🧐

    (Feedback updated until commit 90a3995)

    A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

    Action: Test

    Failed stage: [❌]

    Failed test name: test_user_routes

    Failure summary:

    The action failed because of database connection issues in the user routes tests. Specifically,
    three tests failed:

    1. test_user_routes:25 route: create user
    2. test_user_routes:41 route: get user not exists
    3.
    test_user_routes:53 route: get user exists

    The root cause appears to be a connection handling problem with the PostgreSQL database connection
    pool. The error messages show:

  • "RuntimeError: Event loop is closed" - indicating that the async event loop was closed while
    operations were still pending
  • "RuntimeError: unable to perform operation on ; the handler
    is closed" - showing that the TCP connection was closed unexpectedly
  • "InterfaceError: cannot perform operation: another operation is in progress" - suggesting concurrent
    operations were attempted on the same connection

    These errors point to a race condition or improper cleanup of database connections in the test
    environment.

  • Relevant error logs:
    1:  ##[group]Operating System
    2:  Ubuntu
    ...
    
    1429:  30/u add_unaccent_search_config (113.418433ms)
    1430:  31/u add_trigram_search (113.537691ms)
    1431:  32/u enhance_trigram_search (112.78589ms)
    1432:  33/u fix_latest_transitions (122.724869ms)
    1433:  34/u switch_to_hypercore (122.296478ms)
    1434:  35/u enhanced_indices (127.793573ms)
    1435:  PASS  test_agent_queries:27 query: create agent sql                          3%
    1436:  PASS  test_agent_queries:43 query: create or update agent sql                3%
    1437:  PASS  test_agent_queries:62 query: update agent sql                          4%
    1438:  PASS  test_agent_queries:89 query: get agent not exists sql                  4%
    1439:  PASS  test_agent_queries:100 query: get agent exists sql                     4%
    1440:  PASS  test_agent_queries:121 query: list agents sql                          5%
    1441:  PASS  test_agent_queries:132 query: patch agent sql                          5%
    1442:  PASS  test_agent_queries:156 query: delete agent sql                         5%
    1443:  INFO:httpx:HTTP Request: POST http://testserver/agents "HTTP/1.1 403 Forbidden"
    1444:  PASS  test_agent_routes:9 route: unauthorized should fail                    5%
    1445:  INFO:httpx:HTTP Request: POST http://testserver/agents "HTTP/1.1 201 Created"
    ...
    
    1504:  PASS  test_docs_queries:205 query: delete user doc                          17%
    1505:  PASS  test_docs_queries:242 query: delete agent doc                         17%
    1506:  PASS  test_docs_queries:279 query: search docs by text                      18%
    1507:  PASS  test_docs_queries:316 query: search docs by text with technical       18%
    1508:  terms and phrases                                  
    1509:  PASS  test_docs_queries:378 query: search docs by embedding                 18%
    1510:  PASS  test_docs_queries:406 query: search docs by hybrid                    18%
    1511:  INFO:httpx:HTTP Request: POST http://testserver/users/067ea87f-d7e6-7cbb-8000-19a9921fcbf0/docs "HTTP/1.1 201 Created"
    1512:  PASS  test_docs_routes:15 route: create user doc                            19%
    1513:  INFO:httpx:HTTP Request: POST http://testserver/agents/067ea87f-dd48-778a-8000-e4ef3e05cfcc/docs "HTTP/1.1 201 Created"
    1514:  PASS  test_docs_routes:32 route: create agent doc                           19%
    1515:  INFO:httpx:HTTP Request: POST http://testserver/agents/067ea87f-e2a5-712a-8000-5bb6aba2f54a/docs "HTTP/1.1 201 Created"
    1516:  INFO:httpx:HTTP Request: POST http://testserver/agents/067ea87f-e2a5-712a-8000-5bb6aba2f54a/docs "HTTP/1.1 409 Conflict"
    1517:  INFO:httpx:HTTP Request: POST http://testserver/users/067ea87f-e621-7280-8000-a8f803ce616f/docs "HTTP/1.1 201 Created"
    1518:  PASS  test_docs_routes:49 route: create agent doc with duplicate title      19%
    1519:  should fail                                          
    1520:  INFO:httpx:HTTP Request: POST http://testserver/agents/067ea87f-eb8a-7c43-8000-05cba3276d9f/docs "HTTP/1.1 201 Created"
    ...
    
    1589:  PASS  test_execution_queries:33 query: create execution                     26%
    1590:  PASS  test_execution_queries:58 query: get execution                        26%
    1591:  PASS  test_execution_queries:71 query: lookup temporal id                   27%
    1592:  PASS  test_execution_queries:84 query: list executions                      27%
    1593:  PASS  test_execution_queries:103 query: count executions                    27%
    1594:  PASS  test_execution_queries:121 query: create execution transition         27%
    1595:  PASS  test_execution_queries:142 query: create execution transition -       28%
    1596:  validate transition targets                   
    1597:  PASS  test_execution_queries:187 query: create execution transition with    28%
    1598:  execution update                              
    1599:  PASS  test_execution_queries:214 query: get execution with transitions      28%
    1600:  count                                         
    1601:  PASS  test_execution_queries:229 query: list executions with                29%
    1602:  latest_executions view                        
    1603:  PASS  test_execution_queries:252 query: execution with finish transition    29%
    1604:  PASS  test_execution_queries:286 query: execution with error transition     29%
    1605:  SKIP  test_execution_workflow… workflow: evaluate step   needs to be fixed  30%
    1606:  single                                          
    1607:  SKIP  test_execution_workflow… workflow: evaluate step   needs to be fixed  30%
    1608:  multiple                                        
    1609:  SKIP  test_execution_workflo… workflow: variable access  needs to be fixed  30%
    1610:  in expressions                                   
    1611:  SKIP  test_execution_workflo… workflow: yield step       needs to be fixed  31%
    1612:  SKIP  test_execution_workflo… workflow: sleep step       needs to be fixed  31%
    1613:  SKIP  test_execution_workflo… workflow: return step      needs to be fixed  31%
    1614:  direct                                           
    1615:  SKIP  test_execution_workflo… workflow: return step      needs to be fixed  31%
    1616:  nested                                           
    1617:  SKIP  test_execution_workflo… workflow: log step         needs to be fixed  32%
    1618:  SKIP  test_execution_workflo… workflow: log step         needs to be fixed  32%
    1619:  expression fail                                  
    1620:  SKIP  test_execution_workf… workflow: system call   workflow: thread race   32%
    ...
    
    1700:  request with invalid language                    
    1701:  PASS  test_litellm_utils:8 litellm_utils: acompletion - no tools            45%
    1702:  PASS  test_memory_utils:12 total_size calculates correct size for basic     45%
    1703:  types                                               
    1704:  PASS  test_memory_utils:30 total_size correctly handles container types     45%
    1705:  PASS  test_memory_utils:60 total_size correctly handles nested objects      46%
    1706:  PASS  test_memory_utils:78 total_size handles custom objects                46%
    1707:  PASS  test_memory_utils:106 total_size handles objects with circular        46%
    1708:  references                                         
    1709:  PASS  test_memory_utils:125 total_size with custom handlers                 47%
    1710:  PASS  test_mmr:24 utility: test to apply_mmr_to_docs                        47%
    1711:  PASS  test_mmr:61 utility: test mmr with different mmr_strength values      47%
    1712:  PASS  test_mmr:101 utility: test mmr with empty docs list                   47%
    1713:  PASS  test_model_validation:10 validate_model: succeeds when model is       48%
    1714:  available in model list                         
    1715:  PASS  test_model_validation:19 validate_model: fails when model is          48%
    1716:  unavailable in model list                       
    1717:  PASS  test_model_validation:31 validate_model: fails when model is None     48%
    1718:  PASS  test_nlp_utilities:6 utility: clean_keyword                           49%
    ...
    
    1737:  PASS  test_query_utils:5 utility: sanitize_string - strings                 53%
    1738:  PASS  test_query_utils:15 utility: sanitize_string - nested data            53%
    1739:  structures                                           
    1740:  PASS  test_query_utils:41 utility: sanitize_string - non-string types       53%
    1741:  PASS  test_session_queries:37 query: create session sql                     54%
    1742:  PASS  test_session_queries:60 query: create or update session sql           54%
    1743:  PASS  test_session_queries:84 query: get session exists                     54%
    1744:  PASS  test_session_queries:100 query: get session does not exist            55%
    1745:  PASS  test_session_queries:114 query: list sessions                         55%
    1746:  PASS  test_session_queries:131 query: list sessions with filters            55%
    1747:  PASS  test_session_queries:150 query: count sessions                        56%
    1748:  PASS  test_session_queries:164 query: update session sql                    56%
    1749:  PASS  test_session_queries:199 query: patch session sql                     56%
    1750:  PASS  test_session_queries:226 query: delete session sql                    56%
    1751:  INFO:httpx:HTTP Request: GET http://testserver/sessions "HTTP/1.1 403 Forbidden"
    1752:  PASS  test_session_routes:7 route: unauthorized should fail                 57%
    1753:  INFO:httpx:HTTP Request: POST http://testserver/sessions "HTTP/1.1 201 Created"
    ...
    
    1827:  PASS  test_task_execution_workflow:1620 task execution workflow: evaluate   68%
    1828:  yield expressions assertion            
    1829:  PASS  test_task_queries:24 query: create task sql                           68%
    1830:  PASS  test_task_queries:47 query: create or update task sql                 68%
    1831:  PASS  test_task_queries:70 query: get task sql - exists                     69%
    1832:  PASS  test_task_queries:89 query: get task sql - not exists                 69%
    1833:  PASS  test_task_queries:107 query: delete task sql - exists                 69%
    1834:  PASS  test_task_queries:143 query: delete task sql - not exists             69%
    1835:  PASS  test_task_queries:162 query: list tasks sql - with filters            70%
    1836:  PASS  test_task_queries:183 query: list tasks sql - no filters              70%
    1837:  PASS  test_task_queries:201 query: update task sql - exists                 70%
    1838:  PASS  test_task_queries:237 query: update task sql - not exists             71%
    1839:  PASS  test_task_queries:264 query: patch task sql - exists                  71%
    1840:  PASS  test_task_queries:312 query: patch task sql - not exists              71%
    1841:  INFO:httpx:HTTP Request: POST http://testserver/agents/067ea883-06e7-7454-8000-bcc5cfc6174f/tasks "HTTP/1.1 403 Forbidden"
    1842:  PASS  test_task_routes:27 route: unauthorized should fail                   72%
    1843:  INFO:httpx:HTTP Request: POST http://testserver/agents/067ea883-0a5f-7ff4-8000-1baf324c7c6f/tasks "HTTP/1.1 201 Created"
    ...
    
    1970:  PASS  test_tool_queries:21 query: create tool                               89%
    1971:  PASS  test_tool_queries:47 query: delete tool                               89%
    1972:  PASS  test_tool_queries:79 query: get tool                                  90%
    1973:  PASS  test_tool_queries:92 query: list tools                                90%
    1974:  PASS  test_tool_queries:108 query: patch tool                               90%
    1975:  PASS  test_tool_queries:141 query: update tool                              91%
    1976:  PASS  test_user_queries:36 query: create user sql                           91%
    1977:  PASS  test_user_queries:55 query: create or update user sql                 91%
    1978:  PASS  test_user_queries:75 query: update user sql                           92%
    1979:  PASS  test_user_queries:95 query: get user not exists sql                   92%
    1980:  PASS  test_user_queries:111 query: get user exists sql                      92%
    1981:  PASS  test_user_queries:126 query: list users sql                           92%
    1982:  PASS  test_user_queries:141 query: patch user sql                           93%
    1983:  PASS  test_user_queries:161 query: delete user sql                          93%
    1984:  INFO:httpx:HTTP Request: POST http://testserver/users "HTTP/1.1 403 Forbidden"
    1985:  PASS  test_user_routes:9 route: unauthorized should fail                    93%
    1986:  FAIL  test_user_routes:25 route: create user                                94%
    1987:  FAIL  test_user_routes:41 route: get user not exists                        94%
    1988:  FAIL  test_user_routes:53 route: get user exists                            94%
    1989:  ────────────────────────────── route: create user ──────────────────────────────
    1990:  Failed at tests/test_user_routes.py                                           
    1991:  ╭─────────────────── Traceback (most recent call last) ────────────────────╮  
    1992:  │ in uvloop.loop.Loop.call_soon:1281                                       │  
    1993:  │                                                                          │  
    1994:  │ in uvloop.loop.Loop._call_soon:669                                       │  
    1995:  │                                                                          │  
    1996:  │ in uvloop.loop.UVStream.write:678                                        │  
    1997:  │                                                                          │  
    1998:  │ in uvloop.loop.Loop._append_ready_handle:673                             │  
    1999:  │                                                                          │  
    2000:  │ in uvloop.loop.Loop._check_closed:705                                    │  
    2001:  ╰──────────────────────────────────────────────────────────────────────────╯  
    2002:  RuntimeError: Event loop is closed                                            
    2003:  During handling of the above exception, another exception occurred:           
    ...
    
    2026:  │ │                    │   │   │   │                                     │ │  
    2027:  │ │                    UUID('00000000-0000-0000-0000-000000000000'),     │ │  
    2028:  │ │                    │   │   │   │                                     │ │  
    2029:  │ │                    UUID('067ea884-a861-79c5-8000-ccbf8beaa49d'),     │ │  
    2030:  │ │                    │   │   │   │   'test user',                      │ │  
    2031:  │ │                    │   │   │   │   'test user about',                │ │  
    2032:  │ │                    │   │   │   │   {}                                │ │  
    2033:  │ │                    │   │   │   ],                                    │ │  
    2034:  │ │                    │   │   │   'timeout': 90.0                       │ │  
    2035:  │ │                    │   │   }                                         │ │  
    2036:  │ │                    │   )                                             │ │  
    2037:  │ │                    ]                                                 │ │  
    2038:  │ │             conn = <PoolConnectionProxy                              │ │  
    2039:  │ │                    <asyncpg.connection.Connection object at          │ │  
    2040:  │ │                    0x7f27b4e028a0> 0x7f27c09d7d00>                   │ │  
    2041:  │ │ connection_error = False                                             │ │  
    2042:  │ │  connection_pool = None                                              │ │  
    2043:  │ │            debug = None                                              │ │  
    2044:  │ │           kwargs = {                                                 │ │  
    2045:  │ │                    │   'developer_id':                               │ │  
    2046:  │ │                    UUID('00000000-0000-0000-0000-000000000000'),     │ │  
    2047:  │ │                    │   'data': CreateUserRequest(                    │ │  
    2048:  │ │                    │   │   metadata=None,                            │ │  
    2049:  │ │                    │   │   name='test user',                         │ │  
    2050:  │ │                    │   │   about='test user about'                   │ │  
    2051:  │ │                    │   )                                             │ │  
    2052:  │ │                    }                                                 │ │  
    2053:  │ │    only_on_error = False                                             │ │  
    2054:  │ │             pool = <asyncpg.pool.Pool object at 0x7f27b5bf99c0>      │ │  
    ...
    
    2060:  │ │                    UUID('00000000-0000-0000-0000-000000000000'),     │ │  
    2061:  │ │                    │   │                                             │ │  
    2062:  │ │                    UUID('067ea884-a861-79c5-8000-ccbf8beaa49d'),     │ │  
    2063:  │ │                    │   │   'test user',                              │ │  
    2064:  │ │                    │   │   'test user about',                        │ │  
    2065:  │ │                    │   │   {}                                        │ │  
    2066:  │ │                    │   ]                                             │ │  
    2067:  │ │                    )                                                 │ │  
    2068:  │ │     return_index = -1                                                │ │  
    2069:  │ │           timeit = False                                             │ │  
    2070:  │ ╰──────────────────────────────────────────────────────────────────────╯ │  
    2071:  │                                                                          │  
    2072:  │ /home/runner/work/julep/julep/agents-api/.venv/lib/python3.12/site-packa │  
    2073:  │ ges/asyncpg/transaction.py:68 in __aenter__                              │  
    2074:  │                                                                          │  
    2075:  │    65 │   │   │   raise apg_errors.InterfaceError(                       │  
    2076:  │    66 │   │   │   │   'cannot enter context: already in an `async with`  │  
    2077:  │    67 │   │   self._managed = True                                       │  
    2078:  │ ❱  68 │   │   await self.start()                                         │  
    2079:  │    69 │                                                                  │  
    2080:  │    70 │   async def __aexit__(self, extype, ex, tb):                     │  
    2081:  │    71 │   │   try:                                                       │  
    2082:  │                                                                          │  
    2083:  │ ╭───────────────────────── locals ─────────────────────────╮             │  
    2084:  │ │ self = <asyncpg.Transaction state:failed 0x7f27ab1e9150> │             │  
    2085:  │ ╰──────────────────────────────────────────────────────────╯             │  
    2086:  │                                                                          │  
    2087:  │ /home/runner/work/julep/julep/agents-api/.venv/lib/python3.12/site-packa │  
    2088:  │ ges/asyncpg/transaction.py:146 in start                                  │  
    2089:  │                                                                          │  
    2090:  │   143 │   │   │   query += ';'                                           │  
    2091:  │   144 │   │                                                              │  
    2092:  │   145 │   │   try:                                                       │  
    2093:  │ ❱ 146 │   │   │   await self._connection.execute(query)                  │  
    2094:  │   147 │   │   except BaseException:                                      │  
    2095:  │   148 │   │   │   self._state = TransactionState.FAILED                  │  
    2096:  │   149 │   │   │   raise                                                  │  
    2097:  │                                                                          │  
    2098:  │ ╭───────────────────────────── locals ─────────────────────────────╮     │  
    2099:  │ │   con = <asyncpg.connection.Connection object at 0x7f27b4e028a0> │     │  
    2100:  │ │ query = 'BEGIN;'                                                 │     │  
    2101:  │ │  self = <asyncpg.Transaction state:failed 0x7f27ab1e9150>        │     │  
    2102:  │ ╰──────────────────────────────────────────────────────────────────╯     │  
    ...
    
    2119:  │ │ timeout = None                                                     │   │  
    2120:  │ ╰────────────────────────────────────────────────────────────────────╯   │  
    2121:  │                                                                          │  
    2122:  │ in query:375                                                             │  
    2123:  │                                                                          │  
    2124:  │ in asyncpg.protocol.protocol.BaseProtocol.query:368                      │  
    2125:  │                                                                          │  
    2126:  │ in asyncpg.protocol.protocol.CoreProtocol._simple_query:1174             │  
    2127:  │                                                                          │  
    2128:  │ in asyncpg.protocol.protocol.BaseProtocol._write:967                     │  
    2129:  │                                                                          │  
    2130:  │ in uvloop.loop.UVStream.write:678                                        │  
    2131:  │                                                                          │  
    2132:  │ in uvloop.loop.UVHandle._ensure_alive:159                                │  
    2133:  ╰──────────────────────────────────────────────────────────────────────────╯  
    2134:  RuntimeError: unable to perform operation on <TCPTransport closed=True        
    2135:  reading=False 0x40be7b90>; the handler is closed                              
    ...
    
    2553:  │ │        _TestClientTransport.handle_request.<locals>.receive at       │ │  
    2554:  │ │        0x7f27b4dc5f80>,                                              │ │  
    2555:  │ │        │   <function                                                 │ │  
    2556:  │ │        _TestClientTransport.handle_request.<locals>.send at          │ │  
    2557:  │ │        0x7f27b4dc4d60>                                               │ │  
    2558:  │ │        )                                                             │ │  
    2559:  │ │ func = <fastapi.applications.FastAPI object at 0x7f27df4294c0>       │ │  
    2560:  │ │ self = <anyio._backends._asyncio.BlockingPortal object at            │ │  
    2561:  │ │        0x7f27c9539430>                                               │ │  
    2562:  │ ╰──────────────────────────────────────────────────────────────────────╯ │  
    2563:  │                                                                          │  
    2564:  │ /home/runner/.local/share/uv/python/cpython-3.12.9-linux-x86_64-gnu/lib/ │  
    2565:  │ python3.12/concurrent/futures/_base.py:456 in result                     │  
    2566:  │                                                                          │  
    2567:  │   453 │   │   │   │   if self._state in [CANCELLED, CANCELLED_AND_NOTIFI │  
    2568:  │   454 │   │   │   │   │   raise CancelledError()                         │  
    2569:  │   455 │   │   │   │   elif self._state == FINISHED:                      │  
    2570:  │ ❱ 456 │   │   │   │   │   return self.__get_result()                     │  
    2571:  │   457 │   │   │   │   else:                                              │  
    2572:  │   458 │   │   │   │   │   raise TimeoutError()                           │  
    2573:  │   459 │   │   finally:                                                   │  
    ...
    
    2635:  │ │                       │   │   │   )                                  │ │  
    2636:  │ │                       │   │   ],                                     │ │  
    2637:  │ │                       │   │   'client': ('testclient', 50000),       │ │  
    2638:  │ │                       │   │   ... +9                                 │ │  
    2639:  │ │                       │   },                                         │ │  
    2640:  │ │                       │   <function                                  │ │  
    2641:  │ │                       _TestClientTransport.handle_request.<locals>.… │ │  
    2642:  │ │                       at 0x7f27b4dc5f80>,                            │ │  
    2643:  │ │                       │   <function                                  │ │  
    2644:  │ │                       _TestClientTransport.handle_request.<locals>.… │ │  
    2645:  │ │                       at 0x7f27b4dc4d60>                             │ │  
    2646:  │ │                       )                                              │ │  
    2647:  │ │                func = <fastapi.applications.FastAPI object at        │ │  
    2648:  │ │                       0x7f27df4294c0>                                │ │  
    2649:  │ │              future = <Future at 0x7f27c09dae10 state=finished       │ │  
    2650:  │ │                       raised InterfaceError>                         │ │  
    2651:  │ │              kwargs = {}                                             │ │  
    ...
    
    2725:  │ │         │   │   (b'user-agent', b'testclient'),                      │ │  
    2726:  │ │         │   │   (                                                    │ │  
    2727:  │ │         │   │   │   b'x-auth-key',                                   │ │  
    2728:  │ │         │   │   │   b'98506403086626200041651726252450'              │ │  
    2729:  │ │         │   │   ),                                                   │ │  
    2730:  │ │         │   │   (b'content-length', b'381'),                         │ │  
    2731:  │ │         │   │   (b'content-type', b'application/json')               │ │  
    2732:  │ │         │   ],                                                       │ │  
    2733:  │ │         │   'client': ('testclient', 50000),                         │ │  
    2734:  │ │         │   ... +9                                                   │ │  
    2735:  │ │         }                                                            │ │  
    2736:  │ │  self = <fastapi.applications.FastAPI object at 0x7f27df4294c0>      │ │  
    2737:  │ ╰──────────────────────────────────────────────────────────────────────╯ │  
    2738:  │                                                                          │  
    2739:  │ /home/runner/work/julep/julep/agents-api/.venv/lib/python3.12/site-packa │  
    2740:  │ ges/starlette/middleware/errors.py:187 in __call__                       │  
    2741:  │                                                                          │  
    2742:  │   184 │   │   │   # We always continue to raise the exception.           │  
    2743:  │   185 │   │   │   # This allows servers to log the error, or allows test │  
    2744:  │   186 │   │   │   # to optionally raise the error within the test case.  │  
    2745:  │ ❱ 187 │   │   │   raise exc                                              │  
    ...
    
    2769:  │ │                    │   │   │   b'accept-encoding',                   │ │  
    2770:  │ │                    │   │   │   b'gzip, deflate, zstd'                │ │  
    2771:  │ │                    │   │   ),                                        │ │  
    2772:  │ │                    │   │   (b'connection', b'keep-alive'),           │ │  
    2773:  │ │                    │   │   (b'user-agent', b'testclient'),           │ │  
    2774:  │ │                    │   │   (                                         │ │  
    2775:  │ │                    │   │   │   b'x-auth-key',                        │ │  
    2776:  │ │                    │   │   │   b'98506403086626200041651726252450'   │ │  
    2777:  │ │                    │   │   ),                                        │ │  
    2778:  │ │                    │   │   (b'content-length', b'381'),              │ │  
    2779:  │ │                    │   │   (b'content-type', b'application/json')    │ │  
    2780:  │ │                    │   ],                                            │ │  
    2781:  │ │                    │   'client': ('testclient', 50000),              │ │  
    2782:  │ │                    │   ... +9                                        │ │  
    2783:  │ │                    }                                                 │ │  
    2784:  │ │             self = <starlette.middleware.errors.ServerErrorMiddlewa… │ │  
    2785:  │ │                    object at 0x7f27c9532c30>                         │ │  
    2786:  │ ╰──────────────────────────────────────────────────────────────────────╯ │  
    2787:  │                                                                          │  
    2788:  │ /home/runner/work/julep/julep/agents-api/.venv/lib/python3.12/site-packa │  
    2789:  │ ges/starlette/middleware/errors.py:165 in __call__                       │  
    2790:  │                                                                          │  
    ...
    
    2818:  │ │                    │   │   │   b'accept-encoding',                   │ │  
    2819:  │ │                    │   │   │   b'gzip, deflate, zstd'                │ │  
    2820:  │ │                    │   │   ),                                        │ │  
    2821:  │ │                    │   │   (b'connection', b'keep-alive'),           │ │  
    2822:  │ │                    │   │   (b'user-agent', b'testclient'),           │ │  
    2823:  │ │                    │   │   (                                         │ │  
    2824:  │ │                    │   │   │   b'x-auth-key',                        │ │  
    2825:  │ │                    │   │   │   b'98506403086626200041651726252450'   │ │  
    2826:  │ │                    │   │   ),                                        │ │  
    2827:  │ │                    │   │   (b'content-length', b'381'),              │ │  
    2828:  │ │                    │   │   (b'content-type', b'application/json')    │ │  
    2829:  │ │                    │   ],                                            │ │  
    2830:  │ │                    │   'client': ('testclient', 50000),              │ │  
    2831:  │ │                    │   ... +9                                        │ │  
    2832:  │ │                    }                                                 │ │  
    2833:  │ │             self = <starlette.middleware.errors.ServerErrorMiddlewa… │ │  
    2834:  │ │                    object at 0x7f27c9532c30>                         │ │  
    ...
    
    3063:  │ │         │   ... +9                                                   │ │  
    3064:  │ │         }                                                            │ │  
    3065:  │ │  self = <starlette.middleware.exceptions.ExceptionMiddleware object  │ │  
    3066:  │ │         at 0x7f27c9533b00>                                           │ │  
    3067:  │ ╰──────────────────────────────────────────────────────────────────────╯ │  
    3068:  │                                                                          │  
    3069:  │ /home/runner/work/julep/julep/agents-api/.venv/lib/python3.12/site-packa │  
    3070:  │ ges/starlette/_exception_handler.py:53 in wrapped_app                    │  
    3071:  │                                                                          │  
    3072:  │   50 │   │   │   │   handler = _lookup_exception_handler(exception_handl │  
    3073:  │   51 │   │   │                                                           │  
    3074:  │   52 │   │   │   if handler is None:                                     │  
    3075:  │ ❱ 53 │   │   │   │   raise exc                                           │  
    3076:  │   54 │   │   │                                                           │  
    3077:  │   55 │   │   │   if response_started:                                    │  
    3078:  │   56 │   │   │   │   raise RuntimeError("Caught handled exception, but r │  
    3079:  │                                                                          │  
    ...
    
    3082:  │ │                      0x7f27dee919a0>                                 │ │  
    3083:  │ │               conn = <starlette.requests.Request object at           │ │  
    3084:  │ │                      0x7f27c09d4110>                                 │ │  
    3085:  │ │ exception_handlers = {                                               │ │  
    3086:  │ │                      │   <class                                      │ │  
    3087:  │ │                      'starlette.exceptions.HTTPException'>:          │ │  
    3088:  │ │                      <function http_exception_handler at             │ │  
    3089:  │ │                      0x7f27df666660>,                                │ │  
    3090:  │ │                      │   <class                                      │ │  
    3091:  │ │                      'starlette.exceptions.WebSocketException'>:     │ │  
    3092:  │ │                      <bound method                                   │ │  
    3093:  │ │                      ExceptionMiddleware.websocket_exception of      │ │  
    3094:  │ │                      <starlette.middleware.exceptions.ExceptionMidd… │ │  
    3095:  │ │                      object at 0x7f27c9533b00>>,                     │ │  
    3096:  │ │                      │   <class                                      │ │  
    3097:  │ │                      'fastapi.exceptions.RequestValidationError'>:   │ │  
    3098:  │ │                      <function                                       │ │  
    3099:  │ │                      make_exception_handler.<locals>._handler at     │ │  
    3100:  │ │                      0x7f27cb109b20>,                                │ │  
    3101:  │ │                      │   <class                                      │ │  
    3102:  │ │                      'fastapi.exceptions.WebSocketRequestValidation… │ │  
    3103:  │ │                      <function                                       │ │  
    3104:  │ │                      websocket_request_validation_exception_handler  │ │  
    3105:  │ │                      at 0x7f27df666840>,                             │ │  
    3106:  │ │                      │   <class 'fastapi.exceptions.HTTPException'>: │ │  
    3107:  │ │                      <function http_exception_handler at             │ │  
    3108:  │ │                      0x7f27cb108360>,                                │ │  
    3109:  │ │                      │   <class 'temporalio.service.RPCError'>:      │ │  
    3110:  │ │                      <function validation_error_handler at           │ │  
    3111:  │ │                      0x7f27cb109bc0>,                                │ │  
    3112:  │ │                      │   <class                                      │ │  
    3113:  │ │                      'agents_api.common.exceptions.BaseCommonExcept… │ │  
    3114:  │ │                      <function session_not_found_error_handler at    │ │  
    3115:  │ │                      0x7f27cb109a80>,                                │ │  
    3116:  │ │                      │   <class                                      │ │  
    3117:  │ │                      'agents_api.exceptions.PromptTooBigError'>:     │ │  
    3118:  │ │                      <function prompt_too_big_error at               │ │  
    3119:  │ │                      0x7f27cb1099e0>,                                │ │  
    3120:  │ │                      │   <class 'litellm.exceptions.APIError'>:      │ │  
    3121:  │ │                      <function litellm_api_error at 0x7f27cb109d00>  │ │  
    3122:  │ │                      }                                               │ │  
    ...
    
    3172:  │ │                      0x7f27dee919a0>                                 │ │  
    3173:  │ │               conn = <starlette.requests.Request object at           │ │  
    3174:  │ │                      0x7f27c09d4110>                                 │ │  
    3175:  │ │ exception_handlers = {                                               │ │  
    3176:  │ │                      │   <class                                      │ │  
    3177:  │ │                      'starlette.exceptions.HTTPException'>:          │ │  
    3178:  │ │                      <function http_exception_handler at             │ │  
    3179:  │ │                      0x7f27df666660>,                                │ │  
    3180:  │ │                      │   <class                                      │ │  
    3181:  │ │                      'starlette.exceptions.WebSocketException'>:     │ │  
    3182:  │ │                      <bound method                                   │ │  
    3183:  │ │                      ExceptionMiddleware.websocket_exception of      │ │  
    3184:  │ │                      <starlette.middleware.exceptions.ExceptionMidd… │ │  
    3185:  │ │                      object at 0x7f27c9533b00>>,                     │ │  
    3186:  │ │                      │   <class                                      │ │  
    3187:  │ │                      'fastapi.exceptions.RequestValidationError'>:   │ │  
    3188:  │ │                      <function                                       │ │  
    3189:  │ │                      make_exception_handler.<locals>._handler at     │ │  
    3190:  │ │                      0x7f27cb109b20>,                                │ │  
    3191:  │ │                      │   <class                                      │ │  
    3192:  │ │                      'fastapi.exceptions.WebSocketRequestValidation… │ │  
    3193:  │ │                      <function                                       │ │  
    3194:  │ │                      websocket_request_validation_exception_handler  │ │  
    3195:  │ │                      at 0x7f27df666840>,                             │ │  
    3196:  │ │                      │   <class 'fastapi.exceptions.HTTPException'>: │ │  
    3197:  │ │                      <function http_exception_handler at             │ │  
    3198:  │ │                      0x7f27cb108360>,                                │ │  
    3199:  │ │                      │   <class 'temporalio.service.RPCError'>:      │ │  
    3200:  │ │                      <function validation_error_handler at           │ │  
    3201:  │ │                      0x7f27cb109bc0>,                                │ │  
    3202:  │ │                      │   <class                                      │ │  
    3203:  │ │                      'agents_api.common.exceptions.BaseCommonExcept… │ │  
    3204:  │ │                      <function session_not_found_error_handler at    │ │  
    3205:  │ │                      0x7f27cb109a80>,                                │ │  
    3206:  │ │                      │   <class                                      │ │  
    3207:  │ │                      'agents_api.exceptions.PromptTooBigError'>:     │ │  
    3208:  │ │                      <function prompt_too_big_error at               │ │  
    3209:  │ │                      0x7f27cb1099e0>,                                │ │  
    3210:  │ │                      │   <class 'litellm.exceptions.APIError'>:      │ │  
    3211:  │ │                      <function litellm_api_error at 0x7f27cb109d00>  │ │  
    3212:  │ │                      }                                               │ │  
    ...
    
    3421:  │ │           │   ],                                                     │ │  
    3422:  │ │           │   'client': ('testclient', 50000),                       │ │  
    3423:  │ │           │   ... +9                                                 │ │  
    3424:  │ │           }                                                          │ │  
    3425:  │ ╰──────────────────────────────────────────────────────────────────────╯ │  
    3426:  │                                                                          │  
    3427:  │ /home/runner/work/julep/julep/agents-api/.venv/lib/python3.12/site-packa │  
    3428:  │ ges/starlette/_exception_handler.py:53 in wrapped_app                    │  
    3429:  │                                                                          │  
    3430:  │   50 │   │   │   │   handler = _lookup_exception_handler(exception_handl │  
    3431:  │   51 │   │   │                                                           │  
    3432:  │   52 │   │   │   if handler is None:                                     │  
    3433:  │ ❱ 53 │   │   │   │   raise exc                                           │  
    3434:  │   54 │   │   │                                                           │  
    3435:  │   55 │   │   │   if response_started:                                    │  
    3436:  │   56 │   │   │   │   raise RuntimeError("Caught handled exception, but r │  
    3437:  │                                                                          │  
    3438:  │ ╭─────────────────────────────── locals ───────────────────────────────╮ │  
    3439:  │ │               conn = <starlette.requests.Request object at           │ │  
    3440:  │ │                      0x7f27c09d6390>                                 │ │  
    3441:  │ │ exception_handlers = {                                               │ │  
    3442:  │ │                      │   <class                                      │ │  
    3443:  │ │                      'starlette.exceptions.HTTPException'>:          │ │  
    3444:  │ │                      <function http_exception_handler at             │ │  
    3445:  │ │                      0x7f27df666660>,                                │ │  
    3446:  │ │                      │   <class                                      │ │  
    3447:  │ │                      'starlette.exceptions.WebSocketException'>:     │ │  
    3448:  │ │                      <bound method                                   │ │  
    3449:  │ │                      ExceptionMiddleware.websocket_exception of      │ │  
    3450:  │ │                      <starlette.middleware.exceptions.ExceptionMidd… │ │  
    3451:  │ │                      object at 0x7f27c9533b00>>,                     │ │  
    3452:  │ │                      │   <class                                      │ │  
    3453:  │ │                      'fastapi.exceptions.RequestValidationError'>:   │ │  
    3454:  │ │                      <function                                       │ │  
    3455:  │ │                      make_exception_handler.<locals>._handler at     │ │  
    3456:  │ │                      0x7f27cb109b20>,                                │ │  
    3457:  │ │                      │   <class                                      │ │  
    3458:  │ │                      'fastapi.exceptions.WebSocketRequestValidation… │ │  
    3459:  │ │                      <function                                       │ │  
    3460:  │ │                      websocket_request_validation_exception_handler  │ │  
    3461:  │ │                      at 0x7f27df666840>,                             │ │  
    3462:  │ │                      │   <class 'fastapi.exceptions.HTTPException'>: │ │  
    3463:  │ │                      <function http_exception_handler at             │ │  
    3464:  │ │                      0x7f27cb108360>,                                │ │  
    3465:  │ │                      │   <class 'temporalio.service.RPCError'>:      │ │  
    3466:  │ │                      <function validation_error_handler at           │ │  
    3467:  │ │                      0x7f27cb109bc0>,                                │ │  
    3468:  │ │                      │   <class                                      │ │  
    3469:  │ │                      'agents_api.common.exceptions.BaseCommonExcept… │ │  
    3470:  │ │                      <function session_not_found_error_handler at    │ │  
    3471:  │ │                      0x7f27cb109a80>,                                │ │  
    3472:  │ │                      │   <class                                      │ │  
    3473:  │ │                      'agents_api.exceptions.PromptTooBigError'>:     │ │  
    3474:  │ │                      <function prompt_too_big_error at               │ │  
    3475:  │ │                      0x7f27cb1099e0>,                                │ │  
    3476:  │ │                      │   <class 'litellm.exceptions.APIError'>:      │ │  
    3477:  │ │                      <function litellm_api_error at 0x7f27cb109d00>  │ │  
    3478:  │ │                      }                                               │ │  
    ...
    
    3526:  │ ╭─────────────────────────────── locals ───────────────────────────────╮ │  
    3527:  │ │               conn = <starlette.requests.Request object at           │ │  
    3528:  │ │                      0x7f27c09d6390>                                 │ │  
    3529:  │ │ exception_handlers = {                                               │ │  
    3530:  │ │                      │   <class                                      │ │  
    3531:  │ │                      'starlette.exceptions.HTTPException'>:          │ │  
    3532:  │ │                      <function http_exception_handler at             │ │  
    3533:  │ │                      0x7f27df666660>,                                │ │  
    3534:  │ │                      │   <class                                      │ │  
    3535:  │ │                      'starlette.exceptions.WebSocketException'>:     │ │  
    3536:  │ │                      <bound method                                   │ │  
    3537:  │ │                      ExceptionMiddleware.websocket_exception of      │ │  
    3538:  │ │                      <starlette.middleware.exceptions.ExceptionMidd… │ │  
    3539:  │ │                      object at 0x7f27c9533b00>>,                     │ │  
    3540:  │ │                      │   <class                                      │ │  
    3541:  │ │                      'fastapi.exceptions.RequestValidationError'>:   │ │  
    3542:  │ │                      <function                                       │ │  
    3543:  │ │                      make_exception_handler.<locals>._handler at     │ │  
    3544:  │ │                      0x7f27cb109b20>,                                │ │  
    3545:  │ │                      │   <class                                      │ │  
    3546:  │ │                      'fastapi.exceptions.WebSocketRequestValidation… │ │  
    3547:  │ │                      <function                                       │ │  
    3548:  │ │                      websocket_request_validation_exception_handler  │ │  
    3549:  │ │                      at 0x7f27df666840>,                             │ │  
    3550:  │ │                      │   <class 'fastapi.exceptions.HTTPException'>: │ │  
    3551:  │ │                      <function http_exception_handler at             │ │  
    3552:  │ │                      0x7f27cb108360>,                                │ │  
    3553:  │ │                      │   <class 'temporalio.service.RPCError'>:      │ │  
    3554:  │ │                      <function validation_error_handler at           │ │  
    3555:  │ │                      0x7f27cb109bc0>,                                │ │  
    3556:  │ │                      │   <class                                      │ │  
    3557:  │ │                      'agents_api.common.exceptions.BaseCommonExcept… │ │  
    3558:  │ │                      <function session_not_found_error_handler at    │ │  
    3559:  │ │                      0x7f27cb109a80>,                                │ │  
    3560:  │ │                      │   <class                                      │ │  
    3561:  │ │                      'agents_api.exceptions.PromptTooBigError'>:     │ │  
    3562:  │ │                      <function prompt_too_big_error at               │ │  
    3563:  │ │                      0x7f27cb1099e0>,                                │ │  
    3564:  │ │                      │   <class 'litellm.exceptions.APIError'>:      │ │  
    3565:  │ │                      <function litellm_api_error at 0x7f27cb109d00>  │ │  
    3566:  │ │                      }                                               │ │  
    ...
    
    3635:  │ │           │   │   │   b'x-auth-key',                                 │ │  
    3636:  │ │           │   │   │   b'98506403086626200041651726252450'            │ │  
    3637:  │ │           │   │   ),                                                 │ │  
    3638:  │ │           │   │   (b'content-length', b'381'),                       │ │  
    3639:  │ │           │   │   (b'content-type', b'application/json')             │ │  
    3640:  │ │           │   ],                                                     │ │  
    3641:  │ │           │   'client': ('testclient', 50000),                       │ │  
    3642:  │ │           │   ... +9                                                 │ │  
    3643:  │ │           }                                                          │ │  
    3644:  │ ╰──────────────────────────────────────────────────────────────────────╯ │  
    3645:  │                                                                          │  
    3646:  │ /home/runner/work/julep/julep/agents-api/.venv/lib/python3.12/site-packa │  
    3647:  │ ges/fastapi/routing.py:301 in app                                        │  
    3648:  │                                                                          │  
    3649:  │    298 │   │   │   │   )                                                 │  
    3650:  │    299 │   │   │   │   errors = solved_result.errors                     │  
    3651:  │    300 │   │   │   │   if not errors:                                    │  
    3652:  │ ❱  301 │   │   │   │   │   raw_response = await run_endpoint_function(   │  
    ...
    
    3851:  │ │                                   background_tasks_param_name=None,  │ │  
    3852:  │ │                                   │                                  │ │  
    3853:  │ │                                   security_scopes_param_name=None,   │ │  
    3854:  │ │                                   │   security_scopes=None,          │ │  
    3855:  │ │                                   │   use_cache=True,                │ │  
    3856:  │ │                                   │   path='/users',                 │ │  
    3857:  │ │                                   │   cache_key=(                    │ │  
    3858:  │ │                                   │   │   <function create_user at   │ │  
    3859:  │ │                                   0x7f27cbc732e0>,                   │ │  
    3860:  │ │                                   │   │   ()                         │ │  
    3861:  │ │                                   │   )                              │ │  
    3862:  │ │                                   )                                  │ │  
    3863:  │ │   dependency_overrides_provider = <fastapi.applications.FastAPI      │ │  
    3864:  │ │                                   object at 0x7f27df4294c0>          │ │  
    3865:  │ │               embed_body_fields = False                              │ │  
    3866:  │ │                          errors = []                                 │ │  
    3867:  │ │                      file_stack = <contextlib.AsyncExitStack object  │ │  
    ...
    
    3891:  │ │ response_model_exclude_defaults = False                              │ │  
    3892:  │ │     response_model_exclude_none = False                              │ │  
    3893:  │ │    response_model_exclude_unset = False                              │ │  
    3894:  │ │          response_model_include = None                               │ │  
    3895:  │ │                   solved_result = SolvedDependency(                  │ │  
    3896:  │ │                                   │   values={                       │ │  
    3897:  │ │                                   │   │   'x_developer_id':          │ │  
    3898:  │ │                                   UUID('00000000-0000-0000-0000-000… │ │  
    3899:  │ │                                   │   │   'data': CreateUserRequest( │ │  
    3900:  │ │                                   │   │   │   metadata=None,         │ │  
    3901:  │ │                                   │   │   │   name='test user',      │ │  
    3902:  │ │                                   │   │   │   about='test user       │ │  
    3903:  │ │                                   about'                             │ │  
    3904:  │ │                                   │   │   )                          │ │  
    3905:  │ │                                   │   },                             │ │  
    3906:  │ │                                   │   errors=[],                     │ │  
    3907:  │ │                                   │   background_tasks=None,         │ │  
    ...
    
    4099:  │   21 │   )                                                               │  
    4100:  │                                                                          │  
    4101:  │ ╭─────────────────────────── locals ────────────────────────────╮        │  
    4102:  │ │           data = CreateUserRequest(                           │        │  
    4103:  │ │                  │   metadata=None,                           │        │  
    4104:  │ │                  │   name='test user',                        │        │  
    4105:  │ │                  │   about='test user about'                  │        │  
    4106:  │ │                  )                                            │        │  
    4107:  │ │ x_developer_id = UUID('00000000-0000-0000-0000-000000000000') │        │  
    4108:  │ ╰───────────────────────────────────────────────────────────────╯        │  
    4109:  │                                                                          │  
    4110:  │ /home/runner/work/julep/julep/agents-api/agents_api/queries/utils.py:282 │  
    4111:  │ in async_wrapper                                                         │  
    4112:  │                                                                          │  
    4113:  │   279 │   │   │   │   result: T = await func(*args, **kwargs)            │  
    4114:  │   280 │   │   │   except BaseException as error:                         │  
    4115:  │   281 │   │   │   │   _check_error(error)                                │  
    4116:  │ ❱ 282 │   │   │   │   raise error                                        │  
    4117:  │   283 │   │   │                                                          │  
    ...
    
    4126:  │ │          │   'data': CreateUserRequest(                              │ │  
    4127:  │ │          │   │   metadata=None,                                      │ │  
    4128:  │ │          │   │   name='test user',                                   │ │  
    4129:  │ │          │   │   about='test user about'                             │ │  
    4130:  │ │          │   )                                                       │ │  
    4131:  │ │          }                                                           │ │  
    4132:  │ ╰──────────────────────────────────────────────────────────────────────╯ │  
    4133:  │                                                                          │  
    4134:  │ /home/runner/work/julep/julep/agents-api/agents_api/queries/utils.py:279 │  
    4135:  │ in async_wrapper                                                         │  
    4136:  │                                                                          │  
    4137:  │   276 │   │   @wraps(func)                                               │  
    4138:  │   277 │   │   async def async_wrapper(*args: P.args, **kwargs: P.kwargs) │  
    4139:  │   278 │   │   │   try:                                                   │  
    4140:  │ ❱ 279 │   │   │   │   result: T = await func(*args, **kwargs)            │  
    4141:  │   280 │   │   │   except BaseException as error:                         │  
    4142:  │   281 │   │   │   │   _check_error(error)                                │  
    4143:  │   282 │   │   │   │   raise error                                        │  
    4144:  │                                                                          │  
    ...
    
    4231:  │ │                    │   │   │   │                                     │ │  
    4232:  │ │                    UUID('00000000-0000-0000-0000-000000000000'),     │ │  
    4233:  │ │                    │   │   │   │                                     │ │  
    4234:  │ │                    UUID('067ea884-a861-79c5-8000-ccbf8beaa49d'),     │ │  
    4235:  │ │                    │   │   │   │   'test user',                      │ │  
    4236:  │ │                    │   │   │   │   'test user about',                │ │  
    4237:  │ │                    │   │   │   │   {}                                │ │  
    4238:  │ │                    │   │   │   ],                                    │ │  
    4239:  │ │                    │   │   │   'timeout': 90.0                       │ │  
    4240:  │ │                    │   │   }                                         │ │  
    4241:  │ │                    │   )                                             │ │  
    4242:  │ │                    ]                                                 │ │  
    4243:  │ │             conn = <PoolConnectionProxy                              │ │  
    4244:  │ │                    <asyncpg.connection.Connection object at          │ │  
    4245:  │ │                    0x7f27b4e028a0> 0x7f27c09d7d00>                   │ │  
    4246:  │ │ connection_error = False                                             │ │  
    4247:  │ │  connection_pool = None                                              │ │  
    4248:  │ │            debug = None                                              │ │  
    4249:  │ │           kwargs = {                                                 │ │  
    4250:  │ │                    │   'developer_id':                               │ │  
    4251:  │ │                    UUID('00000000-0000-0000-0000-000000000000'),     │ │  
    4252:  │ │                    │   'data': CreateUserRequest(                    │ │  
    4253:  │ │                    │   │   metadata=None,                            │ │  
    4254:  │ │                    │   │   name='test user',                         │ │  
    4255:  │ │                    │   │   about='test user about'                   │ │  
    4256:  │ │                    │   )                                             │ │  
    4257:  │ │                    }                                                 │ │  
    4258:  │ │    only_on_error = False                                             │ │  
    4259:  │ │             pool = <asyncpg.pool.Pool object at 0x7f27b5bf99c0>      │ │  
    ...
    
    4277:  │ /home/runner/work/julep/julep/agents-api/.venv/lib/python3.12/site-packa │  
    4278:  │ ges/asyncpg/pool.py:1031 in __aexit__                                    │  
    4279:  │                                                                          │  
    4280:  │   1028 │   │   self.done = True                                          │  
    4281:  │   1029 │   │   con = self.connection                                     │  
    4282:  │   1030 │   │   self.connection = None                                    │  
    4283:  │ ❱ 1031 │   │   await self.pool.release(con)                              │  
    4284:  │   1032 │                                                                 │  
    4285:  │   1033 │   def __await__(self):                                          │  
    4286:  │   1034 │   │   self.done = True                                          │  
    4287:  │                                                                          │  
    4288:  │ ╭─────────────────────────────── locals ───────────────────────────────╮ │  
    4289:  │ │  con = <PoolConnectionProxy <asyncpg.connection.Connection object at │ │  
    4290:  │ │        0x7f27b4e028a0> 0x7f27c09d7d00>                               │ │  
    4291:  │ │  exc = (                                                             │ │  
    4292:  │ │        │   <class 'RuntimeError'>,                                   │ │  
    4293:  │ │        │   RuntimeError('unable to perform operation on              │ │  
    4294:  │ │        <TCPTransport closed=True reading=False 0x40be7b90>; the      │ │  
    ...
    
    4309:  │    907 │   async def close(self):                                        │  
    4310:  │    908 │   │   """Attempt to gracefully close all connections in the poo │  
    4311:  │                                                                          │  
    4312:  │ ╭─────────────────────────────── locals ───────────────────────────────╮ │  
    4313:  │ │         ch = <asyncpg.pool.PoolConnectionHolder object at            │ │  
    4314:  │ │              0x7f27b4e183c0>                                         │ │  
    4315:  │ │ connection = <PoolConnectionProxy <asyncpg.connection.Connection     │ │  
    4316:  │ │              object at 0x7f27b4e028a0> 0x7f27c09d7d00>               │ │  
    4317:  │ │       self = <asyncpg.pool.Pool object at 0x7f27b5bf99c0>            │ │  
    4318:  │ │    timeout = None                                                    │ │  
    4319:  │ ╰──────────────────────────────────────────────────────────────────────╯ │  
    4320:  │                                                                          │  
    4321:  │ /home/runner/work/julep/julep/agents-api/.venv/lib/python3.12/site-packa │  
    4322:  │ ges/asyncpg/pool.py:228 in release                                       │  
    4323:  │                                                                          │  
    4324:  │    225 │   │   │   │   # an IO error, so terminate the connection.       │  
    4325:  │    226 │   │   │   │   self._con.terminate()                             │  
    ...
    
    4331:  │                                                                          │  
    4332:  │ ╭─────────────────────────────── locals ───────────────────────────────╮ │  
    4333:  │ │  budget = None                                                       │ │  
    4334:  │ │    self = <asyncpg.pool.PoolConnectionHolder object at               │ │  
    4335:  │ │           0x7f27b4e183c0>                                            │ │  
    4336:  │ │ timeout = None                                                       │ │  
    4337:  │ ╰──────────────────────────────────────────────────────────────────────╯ │  
    4338:  │                                                                          │  
    4339:  │ /home/runner/work/julep/julep/agents-api/.venv/lib/python3.12/site-packa │  
    4340:  │ ges/asyncpg/pool.py:218 in release                                       │  
    4341:  │                                                                          │  
    4342:  │    215 │   │   │   │   │   await self._con._reset()                      │  
    4343:  │    216 │   │   │   │   │   await self._pool._reset(self._con)            │  
    4344:  │    217 │   │   │   else:                                                 │  
    4345:  │ ❱  218 │   │   │   │   await self._con.reset(timeout=budget)             │  
    4346:  │    219 │   │   except (Exception, asyncio.CancelledError) as ex:         │  
    4347:  │    220 │   │   │   # If the `reset` call failed, terminate the connectio │  
    4348:  │    221 │   │   │   # A new one will be created when `acquire` is called  │  
    ...
    
    4395:  │    350 │   │   │   return result                                         │  
    4396:  │    351 │   │                                                             │  
    4397:  │    352 │   │   _, status, _ = await self._execute(                       │  
    4398:  │                                                                          │  
    4399:  │ ╭────────────────────────────── locals ──────────────────────────────╮   │  
    4400:  │ │    args = ()                                                       │   │  
    4401:  │ │   query = 'ROLLBACK'                                               │   │  
    4402:  │ │    self = <asyncpg.connection.Connection object at 0x7f27b4e028a0> │   │  
    4403:  │ │ timeout = None                                                     │   │  
    4404:  │ ╰────────────────────────────────────────────────────────────────────╯   │  
    4405:  │                                                                          │  
    4406:  │ in query:360                                                             │  
    4407:  │                                                                          │  
    4408:  │ in asyncpg.protocol.protocol.BaseProtocol._check_state:745               │  
    4409:  ╰──────────────────────────────────────────────────────────────────────────╯  
    4410:  InterfaceError: cannot perform operation: another operation is in progress    
    4411:  ────────────────────────── route: get user not exists ──────────────────────────
    4412:  Failed at tests/test_user_routes.py                                           
    4413:  ╭─────────────────── Traceback (most recent call last) ────────────────────╮  
    4414:  │ in uvloop.loop.Loop.call_soon:1281                                       │  
    4415:  │                                                                          │  
    4416:  │ in uvloop.loop.Loop._call_soon:669                                       │  
    4417:  │                                                                          │  
    4418:  │ in uvloop.loop.UVStream.write:678                                        │  
    4419:  │                                                                          │  
    4420:  │ in uvloop.loop.Loop._append_ready_handle:673                             │  
    4421:  │                                                                          │  
    4422:  │ in uvloop.loop.Loop._check_closed:705                                    │  
    4423:  ╰────────────────────────────────────────────...

    @whiterabbit1983 whiterabbit1983 force-pushed the f/chat-system-tool-calls branch from 459aa61 to ea7b342 Compare February 21, 2025 13:20
    @whiterabbit1983 whiterabbit1983 marked this pull request as ready for review February 26, 2025 09:55
    Copy link
    Contributor

    PR Reviewer Guide 🔍

    Here are some key observations to aid the review process:

    ⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
    🧪 PR contains tests
    🔒 Security concerns

    Access Control:
    The tool call system provides broad access to sensitive operations (user management, session management, document operations) without proper authorization checks. While developer_id is used, there's no validation that the developer has permissions for the requested operations. This could lead to unauthorized access to resources.

    ⚡ Recommended focus areas for review

    Error Handling

    The tool call evaluation process lacks proper error handling for failed tool calls. A failed tool call could cause the entire chat process to fail without graceful degradation.

    response: ModelResponse | CustomStreamWrapper | None = None
    done = False
    while not done:
        response: ModelResponse | CustomStreamWrapper = await func(**kwargs)
        if not response.choices or not response.choices[0].message.tool_calls:
            return response
    
        # TODO: add streaming response handling
        for tool in response.choices[0].message.tool_calls:
            if tool.type not in tool_types:
                done = True
                continue
    
            done = False
            # call a tool
            tool_name = tool.function.name
            tool_args = json.loads(tool.function.arguments)
            tool_response = await call_tool(developer_id, tool_name, tool_args)
    
            # append result to messages from previous step
            messages: list = kwargs.get("messages", [])
            messages.append({
                "tool_call_id": tool.id,
                "role": "tool",
                "name": tool_name,
                "content": tool_response,
            })
            kwargs["messages"] = messages
    Resource Validation

    The tool call system allows access to sensitive operations without validating resource ownership or permissions beyond the developer ID.

    async def call_tool(developer_id: UUID, tool_name: str, arguments: dict):
        tool_handler = _system_tool_handlers.get(tool_name)
        if not tool_handler:
            msg = f"System call not implemented for {tool_name}"
            raise NotImplementedError(msg)
    
        connection_pool = getattr(app.state, "postgres_pool", None)
        tool_handler = partial(tool_handler, connection_pool=connection_pool)
        arguments["developer_id"] = developer_id
    
        # Convert all UUIDs to UUID objects
        uuid_fields = ["agent_id", "user_id", "task_id", "session_id", "doc_id"]
        for field in uuid_fields:
            if field in arguments:
                fld = arguments[field]
                if isinstance(fld, str):
                    arguments[field] = UUID(fld)
    
        parts = tool_name.split(".")
        if len(parts) < MIN_TOOL_NAME_SEGMENTS:
            msg = f"invalid system tool name: {tool_name}"
            raise NameError(msg)
    
        resource, subresource, operation = parts[0], None, parts[-1]
        if len(parts) > MIN_TOOL_NAME_SEGMENTS:
            subresource = parts[1]
    
        if subresource == "doc" and operation not in ["create", "search"]:
            owner_id_field = f"{resource}_id"
            if owner_id_field in arguments:
                doc_args = {
                    "owner_type": resource,
                    "owner_id": arguments[owner_id_field],
                    **arguments,
                }
                doc_args.pop(owner_id_field)
                arguments = doc_args
    
        # Handle special cases for doc operations
        if operation == "create" and subresource == "doc":
            arguments["x_developer_id"] = arguments.pop("developer_id")
            return await tool_handler(
                data=CreateDocRequest(**arguments.pop("data")),
                **arguments,
            )
    
        # Handle search operations
        if operation == "search" and subresource == "doc":
            arguments["x_developer_id"] = arguments.pop("developer_id")
            search_params = _create_search_request(arguments)
            return await tool_handler(search_params=search_params, **arguments)
    
        # Handle chat operations
        if operation == "chat" and resource == "session":
            developer = await get_developer(
                developer_id=arguments["developer_id"],
                connection_pool=connection_pool,
            )  # type: ignore[not-callable]
    
            session_id = arguments.get("session_id")
            x_custom_api_key = arguments.get("x_custom_api_key", None)
            chat_input = ChatInput(**arguments)
            bg_runner = BackgroundTasks()
            res = await tool_handler(
                developer=developer,
                session_id=session_id,
                background_tasks=bg_runner,
                x_custom_api_key=x_custom_api_key,
                chat_input=chat_input,
            )
            await bg_runner()
            return res
    
        # Handle create session
        if operation == "create" and resource == "session":
            developer_id = arguments.pop("developer_id")
            session_id = arguments.pop("session_id", None)
            create_session_request = CreateSessionRequest(**arguments)
    
            return await tool_handler(
                developer_id=developer_id,
                session_id=session_id,
                data=create_session_request,
            )
    
        # Handle update session
        if operation == "update" and resource == "session":
            developer_id = arguments.pop("developer_id")
            session_id = arguments.pop("session_id")
            update_session_request = UpdateSessionRequest(**arguments)
    
            return await tool_handler(
                developer_id=developer_id,
                session_id=session_id,
                data=update_session_request,
            )
    
        # Handle update user
        if operation == "update" and resource == "user":
            developer_id = arguments.pop("developer_id")
            user_id = arguments.pop("user_id")
            update_user_request = UpdateUserRequest(**arguments)
    
            return await tool_handler(
                developer_id=developer_id,
                user_id=user_id,
                data=update_user_request,
            )
    
        return await tool_handler(**arguments)
    Input Sanitization

    The JSON parsing of tool arguments lacks input validation and sanitization, which could lead to injection of malicious data.

    tool_args = json.loads(tool.function.arguments)
    tool_response = await call_tool(developer_id, tool_name, tool_args)

    Copy link
    Contributor

    qodo-merge-pro-for-open-source bot commented Feb 26, 2025

    PR Code Suggestions ✨

    Explore these optional code suggestions:

    CategorySuggestion                                                                                                                                    Impact
    Security
    Validate tool name before execution
    Suggestion Impact:The commit refactored the code into a class-based approach where tool validation is handled differently. The validation logic is now implemented in the _call_tool method (line 479-480) where tool_name is checked against system_tool_handlers before proceeding, which addresses the security concern raised in the suggestion.

    code diff:

    +    async def _call_tool(self, developer_id: UUID, tool_name: str, arguments: dict):
    +        tool_handler = self.system_tool_handlers.get(tool_name)
    +        if not tool_handler:
    +            msg = f"System call not implemented for {tool_name}"
    +            raise NotImplementedError(msg)

    Add validation for tool_name to prevent potential security issues from arbitrary
    function calls. Ensure tool_name exists in _system_tool_handlers before
    proceeding.

    agents-api/agents_api/routers/utils/tools.py [250-252]

     tool_name = tool.function.name
    +if tool_name not in _system_tool_handlers:
    +    continue
     tool_args = json.loads(tool.function.arguments)
     tool_response = await call_tool(developer_id, tool_name, tool_args)

    [Suggestion has been applied]

    Suggestion importance[1-10]: 9

    __

    Why: This security enhancement prevents potential arbitrary function execution by validating tool names against allowed handlers before processing, addressing a significant security vulnerability.

    High
    Possible issue
    Handle malformed JSON tool arguments
    Suggestion Impact:The suggestion was implemented in the refactored code. The original code was significantly restructured into a class-based approach, but the JSON error handling was incorporated at line 528 where tool arguments are parsed.

    code diff:

    +                tool_name = tool.function.name
    +                tool_args = json.loads(tool.function.arguments)
    +                tool_response = await self._call_tool(developer_id, tool_name, tool_args)

    Add error handling for JSON parsing of tool arguments to prevent crashes when
    malformed JSON is received from the LLM response.

    agents-api/agents_api/routers/utils/tools.py [250-252]

     tool_name = tool.function.name
    -tool_args = json.loads(tool.function.arguments)
    +try:
    +    tool_args = json.loads(tool.function.arguments)
    +except json.JSONDecodeError:
    +    tool_args = {}
     tool_response = await call_tool(developer_id, tool_name, tool_args)

    [Suggestion has been applied]

    Suggestion importance[1-10]: 8

    __

    Why: Adding error handling for JSON parsing is critical to prevent runtime crashes when the LLM returns malformed JSON arguments, ensuring system stability and reliability.

    Medium
    Prevent infinite tool call loops

    Add a maximum recursion limit to prevent infinite loops in case the LLM keeps
    returning tool calls indefinitely.

    agents-api/agents_api/routers/utils/tools.py [225-229]

     def tool_calls_evaluator(
         *,
         tool_types: set[str],
         developer_id: UUID,
    +    max_recursion: int = 10,
     ):
    • Apply this suggestion
    Suggestion importance[1-10]: 8

    __

    Why: Adding a maximum recursion limit is crucial to prevent resource exhaustion and system hangs in case of LLM misbehavior or infinite tool call loops.

    Medium
    • Update

    Copy link
    Contributor

    @ellipsis-dev ellipsis-dev bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    ❌ Changes requested. Reviewed everything up to 8e299d8 in 2 minutes and 51 seconds

    More details
    • Looked at 830 lines of code in 3 files
    • Skipped 0 files when reviewing.
    • Skipped posting 6 drafted comments based on config settings.
    1. agents-api/agents_api/routers/sessions/chat.py:206
    • Draft comment:
      Using tool_calls_evaluator with tool_types={"system"} here seems inconsistent with the tests which use 'function'. Please confirm if 'system' is the intended type and update tests or documentation if necessary.
    • Reason this comment was not posted:
      Comment did not seem useful. Confidence is useful = 40% <= threshold 50%
      The comment is asking the PR author to confirm their intention and update tests or documentation if necessary. This violates the rule against asking for confirmation or ensuring behavior is intended. However, it does point out an inconsistency which could be useful for the author to address.
    2. agents-api/tests/test_tool_calls_evaluator.py:305
    • Draft comment:
      When setting function_mock.name and function_mock.arguments, ensure these properties are plain strings to avoid nested MagicMock issues. This is correctly handled in tests but consider adding a comment in production or test code for clarity.
    • Reason this comment was not posted:
      Confidence changes required: 33% <= threshold 50%
      None
    3. agents-api/agents_api/routers/sessions/chat.py:206
    • Draft comment:
      Production code sets tool_types to {'system'} while tests expect {'function'}. Ensure consistent tool type usage.
    • Reason this comment was not posted:
      Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 20% vs. threshold = 50%
      While there does appear to be a mismatch between production and test code, I don't have enough context to know which is correct. The test could be wrong, or the production code could be wrong. Without understanding the tool_calls_evaluator function and its requirements, I can't be confident that changing to "function" is the right fix. The comment is making assumptions without strong evidence.
      I might be missing important context about the tool_calls_evaluator function's requirements. There could be a good reason for the difference between test and production code.
      Even with more context, suggesting a change without understanding why the current value is "system" is risky. We should err on the side of assuming the production code is correct unless we have strong evidence otherwise.
      Delete the comment. While there is a discrepancy, we don't have strong evidence that the production code is wrong and that "function" is the correct value.
    4. agents-api/agents_api/routers/utils/tools.py:244
    • Draft comment:
      The while loop in tool_calls_evaluator uses a 'done' flag that may prematurely skip matching tool calls if a non-matching call is encountered. Consider refactoring to process all matching calls.
    • Reason this comment was not posted:
      Comment looked like it was already resolved.
    5. agents-api/agents_api/routers/utils/tools.py:172
    • Draft comment:
      Awaiting a BackgroundTasks instance is unusual. Verify that 'await bg_runner()' (in chat operations) behaves as intended.
    • Reason this comment was not posted:
      Comment did not seem useful. Confidence is useful = 0% <= threshold 50%
      The comment is asking the author to verify the behavior of 'await bg_runner()'. This falls under asking the author to ensure the behavior is intended, which is against the rules. The comment does not provide a specific suggestion or ask for a specific test to be written.
    6. agents-api/tests/test_tool_calls_evaluator.py:300
    • Draft comment:
      Ensure that test cases reflect the intended production tool type conventions to avoid mismatches.
    • Reason this comment was not posted:
      Comment did not seem useful. Confidence is useful = 0% <= threshold 50%
      This comment is asking the PR author to ensure that test cases reflect certain conventions, which is similar to asking them to double-check or verify something. This violates the rule against asking the author to ensure or verify things.

    Workflow ID: wflow_QgQOiJHOXjoVF10q


    Want Ellipsis to fix these issues? Tag @ellipsis-dev in a comment. You can customize Ellipsis with 👍 / 👎 feedback, review rules, user-specific overrides, quiet mode, and more.

    Copy link
    Contributor

    @ellipsis-dev ellipsis-dev bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    ❌ Changes requested. Incremental review on ebd3769 in 2 minutes and 37 seconds

    More details
    • Looked at 2409 lines of code in 4 files
    • Skipped 0 files when reviewing.
    • Skipped posting 7 drafted comments based on config settings.
    1. agents-api/agents_api/routers/utils/tools.py:275
    • Draft comment:
      TODO already noted – consider implementing streaming response handling for tool call results. This would improve responsiveness for long-running tool calls.
    • Reason this comment was not posted:
      Comment was not on a location in the diff, so it can't be submitted as a review comment.
    2. agents-api/agents_api/routers/utils/tools.py:277
    • Draft comment:
      Revisit how non-matching tool type calls are handled. Setting 'done = True' might not proceed with additional valid tool calls in later choices; consider clarifying intended behavior.
    • Reason this comment was not posted:
      Comment was not on a location in the diff, so it can't be submitted as a review comment.
    3. agents-api/tests/fixtures.py:91
    • Draft comment:
      The helper 'make_acompletion_multiple_outputs' is useful but consider adding inline comments to document its intended use for tool call test scenarios.
    • Reason this comment was not posted:
      Confidence changes required: 50% <= threshold 50%
      None
    4. agents-api/tests/utils.py:169
    • Draft comment:
      The usage of side_effect for acompletion in patch_embed_acompletion_multiple_outputs is clever. Ensure that this side_effect produces async-compatible responses if used in an async context; document any assumptions here.
    • Reason this comment was not posted:
      Comment did not seem useful. Confidence is useful = 0% <= threshold 50%
      The comment is asking the author to ensure that a function produces async-compatible responses and to document assumptions. This is similar to asking the author to ensure behavior is intended or tested, which violates the rules.
    5. agents-api/agents_api/routers/utils/tools.py:283
    • Draft comment:
      Make sure tool.function.arguments is always a JSON string. If it might already be a dict, consider adding a type check before calling json.loads to avoid potential TypeErrors.
    • Reason this comment was not posted:
      Comment was not on a location in the diff, so it can't be submitted as a review comment.
    6. agents-api/tests/test_tool_calls_evaluator.py:778
    • Draft comment:
      Test 'chat: evaluate agent.doc.search tool call' reuses the 'agent_doc_list' fixture. This may be a mistake; consider using a dedicated 'agent_doc_search' fixture to accurately test the search tool call.
    • Reason this comment was not posted:
      Marked as duplicate.
    7. agents-api/tests/utils.py:191
    • Draft comment:
      The construction of the PG DSN using substring [22:] from the test_psql_url seems brittle. Consider using a more robust URL parsing method rather than hard-coding the offset.
    • Reason this comment was not posted:
      Comment was not on a location in the diff, so it can't be submitted as a review comment.

    Workflow ID: wflow_ZeyI4JUQu3R7gpFG


    Want Ellipsis to fix these issues? Tag @ellipsis-dev in a comment. You can customize Ellipsis with 👍 / 👎 feedback, review rules, user-specific overrides, quiet mode, and more.

    @creatorrr creatorrr marked this pull request as draft March 1, 2025 13:27
    @whiterabbit1983 whiterabbit1983 force-pushed the f/chat-system-tool-calls branch from fdde051 to b0244fd Compare March 14, 2025 16:11
    @whiterabbit1983 whiterabbit1983 force-pushed the f/chat-system-tool-calls branch from 208caa2 to 20b65d9 Compare March 17, 2025 18:11
    Comment on lines 309 to +318
    choices=[choice.model_dump() for choice in model_response.choices],
    )

    total_tokens_per_user.labels(str(developer.id)).inc(
    amount=chat_response.usage.total_tokens if chat_response.usage is not None else 0,
    )
    # For non-streaming responses, update metrics and return the response
    if not chat_input.stream:
    total_tokens_per_user.labels(str(developer.id)).inc(
    amount=chat_response.usage.total_tokens if chat_response.usage is not None else 0,
    )
    return chat_response

    return chat_response
    # Note: For streaming responses, we've already returned the StreamingResponse above
    # This code is unreachable for streaming responses
    return None
    Copy link
    Contributor

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    The function returns None at line 321 which contradicts the return type annotation ChatResponse | StreamingResponse and could cause runtime errors.

    📝 Committable Code Suggestion

    ‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

    Suggested change
    choices=[choice.model_dump() for choice in model_response.choices],
    )
    total_tokens_per_user.labels(str(developer.id)).inc(
    amount=chat_response.usage.total_tokens if chat_response.usage is not None else 0,
    )
    # For non-streaming responses, update metrics and return the response
    if not chat_input.stream:
    total_tokens_per_user.labels(str(developer.id)).inc(
    amount=chat_response.usage.total_tokens if chat_response.usage is not None else 0,
    )
    return chat_response
    return chat_response
    # Note: For streaming responses, we've already returned the StreamingResponse above
    # This code is unreachable for streaming responses
    return None
    ) -> ChatResponse | StreamingResponse: # FIXME: Update type to include StreamingResponse
    """
    Initiates a chat session.
    ...
    # This code is unreachable for streaming responses
    raise RuntimeError("Unreachable code - streaming response should have been returned earlier")

    @whiterabbit1983 whiterabbit1983 force-pushed the f/chat-system-tool-calls branch from a9674a2 to bb14350 Compare March 17, 2025 18:28
    Comment on lines 179 to 289
    and len(chunk.choices) > 0
    )

    # Update metrics when we detect the final chunk
    if final_usage and has_choices and chunk.choices[0].finish_reason:
    # This is the last chunk with the finish reason
    total_tokens = final_usage.get("total_tokens", 0)
    total_tokens_per_user.labels(str(developer.id)).inc(
    amount=total_tokens
    )

    # Collect content for the full response
    if has_choices and hasattr(chunk.choices[0], "delta"):
    delta = chunk.choices[0].delta
    if hasattr(delta, "content") and delta.content:
    content_so_far += delta.content
    has_content = True

    # Prepare the response chunk
    choices_to_send = []
    if has_choices:
    chunk_data = chunk.choices[0].model_dump()

    # Ensure delta always contains a role field
    if "delta" in chunk_data and "role" not in chunk_data["delta"]:
    chunk_data["delta"]["role"] = "assistant"

    choices_to_send = [chunk_data]

    # Create and send the chunk response
    chunk_response = ChunkChatResponse(
    id=response_id,
    created_at=created_at,
    jobs=jobs,
    docs=doc_references,
    usage=final_usage,
    choices=choices_to_send,
    )
    yield chunk_response.model_dump_json() + "\n"

    except Exception as e:
    # Log error details for debugging but send a generic message to client
    import logging

    logging.error(f"Error processing chunk: {e!s}")

    error_response = {
    "id": str(response_id),
    "created_at": created_at.isoformat(),
    "error": "An error occurred while processing the response chunk.",
    }
    yield json.dumps(error_response) + "\n"
    # Continue processing remaining chunks
    continue

    # Save complete response to history if needed
    if chat_input.save and has_content:
    try:
    # Create entry for the complete response
    complete_entry = CreateEntryRequest.from_model_input(
    model=settings["model"],
    role="assistant",
    content=content_so_far,
    source="api_response",
    )
    # Create a task to save the entry without blocking the stream
    ref = asyncio.create_task(
    create_entries(
    developer_id=developer.id,
    session_id=session_id,
    data=[complete_entry],
    )
    )
    stream_tasks.append(ref)
    except Exception as e:
    # Log the full error for debugging purposes
    import logging

    logging.error(f"Failed to save streamed response: {e!s}")

    # Send a minimal error message to the client
    error_response = {
    "id": str(response_id),
    "created_at": created_at.isoformat(),
    "error": "Failed to save response history.",
    }
    yield json.dumps(error_response) + "\n"
    except Exception as e:
    # Log the detailed error for system debugging
    import logging

    logging.error(f"Streaming error: {e!s}")

    # Send a user-friendly error message to the client
    error_response = {
    "id": str(response_id),
    "created_at": created_at.isoformat(),
    "error": "An error occurred during the streaming response.",
    }
    yield json.dumps(error_response) + "\n"
    Copy link
    Contributor

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Error handling in streaming mode doesn't properly clean up resources - missing finally block to ensure model resources are released even if streaming fails.

    📝 Committable Code Suggestion

    ‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

    Suggested change
    try:
    # Stream chunks from the model_response (CustomStreamWrapper from litellm)
    async for chunk in model_response:
    # Process a single chunk of the streaming response
    try:
    # Extract usage metrics if available
    if hasattr(chunk, "usage") and chunk.usage:
    final_usage = chunk.usage.model_dump()
    # Check if chunk has valid choices
    has_choices = (
    hasattr(chunk, "choices")
    and chunk.choices
    and len(chunk.choices) > 0
    )
    # Update metrics when we detect the final chunk
    if final_usage and has_choices and chunk.choices[0].finish_reason:
    # This is the last chunk with the finish reason
    total_tokens = final_usage.get("total_tokens", 0)
    total_tokens_per_user.labels(str(developer.id)).inc(
    amount=total_tokens
    )
    # Collect content for the full response
    if has_choices and hasattr(chunk.choices[0], "delta"):
    delta = chunk.choices[0].delta
    if hasattr(delta, "content") and delta.content:
    content_so_far += delta.content
    has_content = True
    # Prepare the response chunk
    choices_to_send = []
    if has_choices:
    chunk_data = chunk.choices[0].model_dump()
    # Ensure delta always contains a role field
    if "delta" in chunk_data and "role" not in chunk_data["delta"]:
    chunk_data["delta"]["role"] = "assistant"
    choices_to_send = [chunk_data]
    # Create and send the chunk response
    chunk_response = ChunkChatResponse(
    id=response_id,
    created_at=created_at,
    jobs=jobs,
    docs=doc_references,
    usage=final_usage,
    choices=choices_to_send,
    )
    yield chunk_response.model_dump_json() + "\n"
    except Exception as e:
    # Log error details for debugging but send a generic message to client
    import logging
    logging.error(f"Error processing chunk: {e!s}")
    error_response = {
    "id": str(response_id),
    "created_at": created_at.isoformat(),
    "error": "An error occurred while processing the response chunk.",
    }
    yield json.dumps(error_response) + "\n"
    # Continue processing remaining chunks
    continue
    # Save complete response to history if needed
    if chat_input.save and has_content:
    try:
    # Create entry for the complete response
    complete_entry = CreateEntryRequest.from_model_input(
    model=settings["model"],
    role="assistant",
    content=content_so_far,
    source="api_response",
    )
    # Create a task to save the entry without blocking the stream
    ref = asyncio.create_task(
    create_entries(
    developer_id=developer.id,
    session_id=session_id,
    data=[complete_entry],
    )
    )
    stream_tasks.append(ref)
    except Exception as e:
    # Log the full error for debugging purposes
    import logging
    logging.error(f"Failed to save streamed response: {e!s}")
    # Send a minimal error message to the client
    error_response = {
    "id": str(response_id),
    "created_at": created_at.isoformat(),
    "error": "Failed to save response history.",
    }
    yield json.dumps(error_response) + "\n"
    except Exception as e:
    # Log the detailed error for system debugging
    import logging
    logging.error(f"Streaming error: {e!s}")
    # Send a user-friendly error message to the client
    error_response = {
    "id": str(response_id),
    "created_at": created_at.isoformat(),
    "error": "An error occurred during the streaming response.",
    }
    yield json.dumps(error_response) + "\n"
    try:
    # Stream chunks from the model_response (CustomStreamWrapper from litellm)
    async for chunk in model_response:
    # Process a single chunk of the streaming response
    try:
    # Extract usage metrics if available
    if hasattr(chunk, "usage") and chunk.usage:
    final_usage = chunk.usage.model_dump()
    # Check if chunk has valid choices
    has_choices = (
    hasattr(chunk, "choices")
    and chunk.choices
    and len(chunk.choices) > 0
    )
    # Update metrics when we detect the final chunk
    if final_usage and has_choices and chunk.choices[0].finish_reason:
    # This is the last chunk with the finish reason
    total_tokens = final_usage.get("total_tokens", 0)
    total_tokens_per_user.labels(str(developer.id)).inc(
    amount=total_tokens
    )
    # Collect content for the full response
    if has_choices and hasattr(chunk.choices[0], "delta"):
    delta = chunk.choices[0].delta
    if hasattr(delta, "content") and delta.content:
    content_so_far += delta.content
    has_content = True
    # Prepare the response chunk
    choices_to_send = []
    if has_choices:
    chunk_data = chunk.choices[0].model_dump()
    # Ensure delta always contains a role field
    if "delta" in chunk_data and "role" not in chunk_data["delta"]:
    chunk_data["delta"]["role"] = "assistant"
    choices_to_send = [chunk_data]
    # Create and send the chunk response
    chunk_response = ChunkChatResponse(
    id=response_id,
    created_at=created_at,
    jobs=jobs,
    docs=doc_references,
    usage=final_usage,
    choices=choices_to_send,
    )
    yield chunk_response.model_dump_json() + "\n"
    except Exception as e:
    # Log error details for debugging but send a generic message to client
    import logging
    logging.error(f"Error processing chunk: {e!s}")
    error_response = {
    "id": str(response_id),
    "created_at": created_at.isoformat(),
    "error": "An error occurred while processing the response chunk.",
    }
    yield json.dumps(error_response) + "\n"
    # Continue processing remaining chunks
    continue
    # Save complete response to history if needed
    if chat_input.save and has_content:
    try:
    # Create entry for the complete response
    complete_entry = CreateEntryRequest.from_model_input(
    model=settings["model"],
    role="assistant",
    content=content_so_far,
    source="api_response",
    )
    # Create a task to save the entry without blocking the stream
    ref = asyncio.create_task(
    create_entries(
    developer_id=developer.id,
    session_id=session_id,
    data=[complete_entry],
    )
    )
    stream_tasks.append(ref)
    except Exception as e:
    # Log the full error for debugging purposes
    import logging
    logging.error(f"Failed to save streamed response: {e!s}")
    # Send a minimal error message to the client
    error_response = {
    "id": str(response_id),
    "created_at": created_at.isoformat(),
    "error": "Failed to save response history.",
    }
    yield json.dumps(error_response) + "\n"
    except Exception as e:
    # Log the detailed error for system debugging
    import logging
    logging.error(f"Streaming error: {e!s}")
    # Send a user-friendly error message to the client
    error_response = {
    "id": str(response_id),
    "created_at": created_at.isoformat(),
    "error": "An error occurred during the streaming response.",
    }
    yield json.dumps(error_response) + "\n"
    finally:
    # Ensure model resources are properly released
    if hasattr(model_response, "close") and callable(model_response.close):
    await model_response.close()
    # Wait for any pending save tasks to complete
    if stream_tasks:
    await asyncio.gather(*stream_tasks, return_exceptions=True)

    Comment on lines 293 to 332
    tool_calls = []

    if not stream:
    response: ModelResponse = await self._completion_func(**kwargs)
    if not response.choices:
    return response

    tool_calls = response.choices[0].message.tool_calls

    if not tool_calls:
    return response
    else:
    first_chunk, response = await self.get_first_chunk(response)
    if first_chunk and not first_chunk.choices:
    return response

    delta = first_chunk.choices[0].delta
    if delta.content:
    return response

    async for chunk in cast(CustomStreamWrapper, response):
    delta = chunk.choices[0].delta
    if delta.tool_calls:
    for tool_call in delta.tool_calls:
    if len(tool_calls) <= tool_call.index:
    tool_calls.append({
    "id": "",
    "type": "function",
    "function": {"name": "", "arguments": ""},
    })

    tc = tool_calls[tool_call.index]
    if tool_call.id:
    tc["id"] = tool_call.id
    if tool_call.function.name:
    tc["function"]["name"] += tool_call.function.name
    if tool_call.function.arguments:
    tc["function"]["arguments"] += tool_call.function.arguments

    for tool in tool_calls:
    Copy link
    Contributor

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    The tool_calls list is populated with tool calls from streaming response but never checked if they are valid tool types, allowing unauthorized tool execution.

    📝 Committable Code Suggestion

    ‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

    Suggested change
    tool_calls = []
    if not stream:
    response: ModelResponse = await self._completion_func(**kwargs)
    if not response.choices:
    return response
    tool_calls = response.choices[0].message.tool_calls
    if not tool_calls:
    return response
    else:
    first_chunk, response = await self.get_first_chunk(response)
    if first_chunk and not first_chunk.choices:
    return response
    delta = first_chunk.choices[0].delta
    if delta.content:
    return response
    async for chunk in cast(CustomStreamWrapper, response):
    delta = chunk.choices[0].delta
    if delta.tool_calls:
    for tool_call in delta.tool_calls:
    if len(tool_calls) <= tool_call.index:
    tool_calls.append({
    "id": "",
    "type": "function",
    "function": {"name": "", "arguments": ""},
    })
    tc = tool_calls[tool_call.index]
    if tool_call.id:
    tc["id"] = tool_call.id
    if tool_call.function.name:
    tc["function"]["name"] += tool_call.function.name
    if tool_call.function.arguments:
    tc["function"]["arguments"] += tool_call.function.arguments
    for tool in tool_calls:
    tool_calls = []
    for chunk in response:
    if not chunk.choices:
    continue
    delta = chunk.choices[0].delta
    if not delta:
    continue
    if delta.tool_calls:
    for tool_call in delta.tool_calls:
    # Validate tool type before adding
    if tool_call.type not in ['function', 'code']:
    continue
    # Initialize tool call if new
    if tool_call.index >= len(tool_calls):
    tool_calls.append({
    'id': tool_call.id,
    'type': tool_call.type,
    'function': {
    'name': '',
    'arguments': ''
    }
    })
    # Update function name if present
    if tool_call.function and tool_call.function.name:
    tool_calls[tool_call.index]['function']['name'] = \
    tool_call.function.name
    # Append function arguments if present
    if tool_call.function and tool_call.function.arguments:
    tool_calls[tool_call.index]['function']['arguments'] += \
    tool_call.function.arguments
    # Execute validated tool calls
    for tool_call in tool_calls:
    if tool_call['type'] == 'function':
    function_name = tool_call['function']['name']
    function_args = tool_call['function']['arguments']
    # Execute function call
    try:
    function_args = json.loads(function_args)
    result = self._call_function(function_name, function_args)
    yield {'type': 'function', 'output': result}
    except Exception as e:
    yield {'type': 'error', 'error': str(e)}

    Comment on lines 269 to 280
    async def peek_first_chunk(
    response: CustomStreamWrapper,
    ) -> tuple[ModelResponseStream, CustomStreamWrapper]:
    try:
    first_chunk = await response.__anext__()
    except StopAsyncIteration:
    Copy link
    Contributor

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Missing error handling for first_chunk being None in peek_first_chunk(). The function should handle the case where first_chunk is None to avoid potential runtime errors.

    📝 Committable Code Suggestion

    ‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

    Suggested change
    async def peek_first_chunk(
    response: CustomStreamWrapper,
    ) -> tuple[ModelResponseStream, CustomStreamWrapper]:
    try:
    first_chunk = await response.__anext__()
    except StopAsyncIteration:
    def peek_first_chunk(self):
    if self.first_chunk is None:
    return None
    return self.first_chunk.chunk

    "id": "call_user_doc_list",
    "type": "system",
    "function": {
    "name": "user.doc.list",
    Copy link
    Contributor

    @creatorrr creatorrr Mar 18, 2025

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    I think this (and related) should be user.docs.list (docs plural) etc to keep things consistent with the api and sdk

    ])

    agent_doc_create = make_acompletion_multiple_outputs(
    lambda agent, doc, user, task, user_doc, session: [
    Copy link
    Contributor

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    could be simplified as lambda agent, **_: ...

    @whiterabbit1983 whiterabbit1983 force-pushed the f/chat-system-tool-calls branch from 3ff3a18 to 65497b5 Compare March 26, 2025 11:10
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Projects
    None yet
    Development

    Successfully merging this pull request may close these issues.

    [Feature]: (API Calls | Integrations | System Calls) Need to be Supported in Sessions Chat
    3 participants