diff --git a/src/crewai/llm.py b/src/crewai/llm.py index 89de55f54..98b0bc855 100644 --- a/src/crewai/llm.py +++ b/src/crewai/llm.py @@ -172,32 +172,50 @@ class LLM: def call( self, - messages: List[Dict[str, str]], + messages: Union[str, List[Dict[str, str]]], tools: Optional[List[dict]] = None, callbacks: Optional[List[Any]] = None, available_functions: Optional[Dict[str, Any]] = None, ) -> str: """ - High-level call method that: - 1) Calls litellm.completion - 2) Checks for function/tool calls - 3) If a tool call is found: - a) executes the function - b) returns the result - 4) If no tool call, returns the text response + High-level llm call method that: + 1) Accepts either a string or a list of messages + 2) Converts string input to the required message format + 3) Calls litellm.completion + 4) Handles function/tool calls if any + 5) Returns the final text response or tool result - :param messages: The conversation messages - :param tools: Optional list of function schemas for function calling - :param callbacks: Optional list of callbacks - :param available_functions: A dictionary mapping function_name -> actual Python function - :return: Final text response from the LLM or the tool result + Parameters: + - messages (Union[str, List[Dict[str, str]]]): The input messages for the LLM. + - If a string is provided, it will be converted into a message list with a single entry. + - If a list of dictionaries is provided, each dictionary should have 'role' and 'content' keys. + - tools (Optional[List[dict]]): A list of tool schemas for function calling. + - callbacks (Optional[List[Any]]): A list of callback functions to be executed. + - available_functions (Optional[Dict[str, Any]]): A dictionary mapping function names to actual Python functions. + + Returns: + - str: The final text response from the LLM or the result of a tool function call. + + Examples: + --------- + # Example 1: Using a string input + response = llm.call("Return the name of a random city in the world.") + print(response) + + # Example 2: Using a list of messages + messages = [{"role": "user", "content": "What is the capital of France?"}] + response = llm.call(messages) + print(response) """ + if isinstance(messages, str): + messages = [{"role": "user", "content": messages}] + with suppress_warnings(): if callbacks and len(callbacks) > 0: self.set_callbacks(callbacks) try: - # --- 1) Make the completion call + # --- 1) Prepare the parameters for the completion call params = { "model": self.model, "messages": messages, @@ -218,11 +236,13 @@ class LLM: "api_version": self.api_version, "api_key": self.api_key, "stream": False, - "tools": tools, # pass the tool schema + "tools": tools, } + # Remove None values from params params = {k: v for k, v in params.items() if v is not None} + # --- 2) Make the completion call response = litellm.completion(**params) response_message = cast(Choices, cast(ModelResponse, response).choices)[ 0 @@ -230,7 +250,7 @@ class LLM: text_response = response_message.content or "" tool_calls = getattr(response_message, "tool_calls", []) - # Ensure callbacks get the full response object with usage info + # --- 3) Handle callbacks with usage info if callbacks and len(callbacks) > 0: for callback in callbacks: if hasattr(callback, "log_success_event"): @@ -243,11 +263,11 @@ class LLM: end_time=0, ) - # --- 2) If no tool calls, return the text response + # --- 4) If no tool calls, return the text response if not tool_calls or not available_functions: return text_response - # --- 3) Handle the tool call + # --- 5) Handle the tool call tool_call = tool_calls[0] function_name = tool_call.function.name @@ -262,7 +282,6 @@ class LLM: try: # Call the actual tool function result = fn(**function_args) - return result except Exception as e: diff --git a/tests/cassettes/test_llm_call_with_message_list.yaml b/tests/cassettes/test_llm_call_with_message_list.yaml new file mode 100644 index 000000000..da204c647 --- /dev/null +++ b/tests/cassettes/test_llm_call_with_message_list.yaml @@ -0,0 +1,102 @@ +interactions: +- request: + body: '{"messages": [{"role": "user", "content": "What is the capital of France?"}], + "model": "gpt-4o-mini"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '101' + content-type: + - application/json + cookie: + - _cfuvid=8NrWEBP3dDmc8p2.csR.EdsSwS8zFvzWI1kPICaK_fM-1737568015338-0.0.1.1-604800000; + __cf_bm=pKr3NwXmTZN9rMSlKvEX40VPKbrxF93QwDNHunL2v8Y-1737568015-1.0.1.1-nR0EA7hYIwWpIBYUI53d9xQrUnl5iML6lgz4AGJW4ZGPBDxFma3PZ2cBhlr_hE7wKa5fV3r32eMu_rNWMXD.eA + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.59.6 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.59.6 + x-stainless-raw-response: + - 'true' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.7 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + content: "{\n \"id\": \"chatcmpl-AsZ6WjNfEOrHwwEEdSZZCRBiTpBMS\",\n \"object\": + \"chat.completion\",\n \"created\": 1737568016,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": \"The capital of France is Paris.\",\n + \ \"refusal\": null\n },\n \"logprobs\": null,\n \"finish_reason\": + \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 14,\n \"completion_tokens\": + 8,\n \"total_tokens\": 22,\n \"prompt_tokens_details\": {\n \"cached_tokens\": + 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n + \ \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": + 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": + \"default\",\n \"system_fingerprint\": \"fp_72ed7ab54c\"\n}\n" + headers: + CF-Cache-Status: + - DYNAMIC + CF-RAY: + - 90615dc63b805cb1-RDU + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Wed, 22 Jan 2025 17:46:56 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '355' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999974' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_cdbed69c9c63658eb552b07f1220df19 + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/tests/cassettes/test_llm_call_with_string_input.yaml b/tests/cassettes/test_llm_call_with_string_input.yaml new file mode 100644 index 000000000..f0c2a51e6 --- /dev/null +++ b/tests/cassettes/test_llm_call_with_string_input.yaml @@ -0,0 +1,108 @@ +interactions: +- request: + body: '{"messages": [{"role": "user", "content": "Return the name of a random + city in the world."}], "model": "gpt-4o-mini"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '117' + content-type: + - application/json + cookie: + - _cfuvid=3UeEmz_rnmsoZxrVUv32u35gJOi766GDWNe5_RTjiPk-1736537376739-0.0.1.1-604800000 + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.59.6 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.59.6 + x-stainless-raw-response: + - 'true' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.7 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + content: "{\n \"id\": \"chatcmpl-AsZ6UtbaNSMpNU9VJKxvn52t5eJTq\",\n \"object\": + \"chat.completion\",\n \"created\": 1737568014,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": \"How about \\\"Lisbon\\\"? It\u2019s the + capital city of Portugal, known for its rich history and vibrant culture.\",\n + \ \"refusal\": null\n },\n \"logprobs\": null,\n \"finish_reason\": + \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 18,\n \"completion_tokens\": + 24,\n \"total_tokens\": 42,\n \"prompt_tokens_details\": {\n \"cached_tokens\": + 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n + \ \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": + 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": + \"default\",\n \"system_fingerprint\": \"fp_72ed7ab54c\"\n}\n" + headers: + CF-Cache-Status: + - DYNAMIC + CF-RAY: + - 90615dbcaefb5cb1-RDU + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Wed, 22 Jan 2025 17:46:55 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=pKr3NwXmTZN9rMSlKvEX40VPKbrxF93QwDNHunL2v8Y-1737568015-1.0.1.1-nR0EA7hYIwWpIBYUI53d9xQrUnl5iML6lgz4AGJW4ZGPBDxFma3PZ2cBhlr_hE7wKa5fV3r32eMu_rNWMXD.eA; + path=/; expires=Wed, 22-Jan-25 18:16:55 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=8NrWEBP3dDmc8p2.csR.EdsSwS8zFvzWI1kPICaK_fM-1737568015338-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '449' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999971' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_898373758d2eae3cd84814050b2588e3 + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/tests/cassettes/test_llm_call_with_string_input_and_callbacks.yaml b/tests/cassettes/test_llm_call_with_string_input_and_callbacks.yaml new file mode 100644 index 000000000..a930a60a7 --- /dev/null +++ b/tests/cassettes/test_llm_call_with_string_input_and_callbacks.yaml @@ -0,0 +1,102 @@ +interactions: +- request: + body: '{"messages": [{"role": "user", "content": "Tell me a joke."}], "model": + "gpt-4o-mini"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '86' + content-type: + - application/json + cookie: + - _cfuvid=8NrWEBP3dDmc8p2.csR.EdsSwS8zFvzWI1kPICaK_fM-1737568015338-0.0.1.1-604800000; + __cf_bm=pKr3NwXmTZN9rMSlKvEX40VPKbrxF93QwDNHunL2v8Y-1737568015-1.0.1.1-nR0EA7hYIwWpIBYUI53d9xQrUnl5iML6lgz4AGJW4ZGPBDxFma3PZ2cBhlr_hE7wKa5fV3r32eMu_rNWMXD.eA + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.59.6 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.59.6 + x-stainless-raw-response: + - 'true' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.7 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + content: "{\n \"id\": \"chatcmpl-AsZ6VyjuUcXYpChXmD8rUSy6nSGq8\",\n \"object\": + \"chat.completion\",\n \"created\": 1737568015,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": \"Why did the scarecrow win an award? \\n\\nBecause + he was outstanding in his field!\",\n \"refusal\": null\n },\n \"logprobs\": + null,\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": + 12,\n \"completion_tokens\": 19,\n \"total_tokens\": 31,\n \"prompt_tokens_details\": + {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": + {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": + 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": + \"default\",\n \"system_fingerprint\": \"fp_72ed7ab54c\"\n}\n" + headers: + CF-Cache-Status: + - DYNAMIC + CF-RAY: + - 90615dc03b6c5cb1-RDU + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Wed, 22 Jan 2025 17:46:56 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '825' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999979' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_4c1485d44e7461396d4a7316a63ff353 + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/tests/cassettes/test_llm_call_with_tool_and_message_list.yaml b/tests/cassettes/test_llm_call_with_tool_and_message_list.yaml new file mode 100644 index 000000000..6102d9ef1 --- /dev/null +++ b/tests/cassettes/test_llm_call_with_tool_and_message_list.yaml @@ -0,0 +1,111 @@ +interactions: +- request: + body: '{"messages": [{"role": "user", "content": "What is the square of 5?"}], + "model": "gpt-4o-mini", "tools": [{"type": "function", "function": {"name": + "square_number", "description": "Returns the square of a number.", "parameters": + {"type": "object", "properties": {"number": {"type": "integer", "description": + "The number to square"}}, "required": ["number"]}}}]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '361' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.59.6 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.59.6 + x-stainless-raw-response: + - 'true' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.7 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + content: "{\n \"id\": \"chatcmpl-AsZL5nGOaVpcGnDOesTxBZPHhMoaS\",\n \"object\": + \"chat.completion\",\n \"created\": 1737568919,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": null,\n \"tool_calls\": [\n {\n + \ \"id\": \"call_i6JVJ1KxX79A4WzFri98E03U\",\n \"type\": + \"function\",\n \"function\": {\n \"name\": \"square_number\",\n + \ \"arguments\": \"{\\\"number\\\":5}\"\n }\n }\n + \ ],\n \"refusal\": null\n },\n \"logprobs\": null,\n + \ \"finish_reason\": \"tool_calls\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": + 58,\n \"completion_tokens\": 15,\n \"total_tokens\": 73,\n \"prompt_tokens_details\": + {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": + {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": + 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": + \"default\",\n \"system_fingerprint\": \"fp_72ed7ab54c\"\n}\n" + headers: + CF-Cache-Status: + - DYNAMIC + CF-RAY: + - 906173d229b905f6-IAD + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Wed, 22 Jan 2025 18:02:00 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=BYDpIoqfPZyRxl9xcFxkt4IzTUGe8irWQlZ.aYLt8Xc-1737568920-1.0.1.1-Y_cVFN7TbguWRBorSKZynVY02QUtYbsbHuR2gR1wJ8LHuqOF4xIxtK5iHVCpWWgIyPDol9xOXiqUkU8xRV_vHA; + path=/; expires=Wed, 22-Jan-25 18:32:00 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=etTqqA9SBOnENmrFAUBIexdW0v2ZeO1x9_Ek_WChlfU-1737568920137-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '642' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999976' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_388e63f9b8d4edc0dd153001f25388e5 + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/tests/cassettes/test_llm_call_with_tool_and_string_input.yaml b/tests/cassettes/test_llm_call_with_tool_and_string_input.yaml new file mode 100644 index 000000000..865d25826 --- /dev/null +++ b/tests/cassettes/test_llm_call_with_tool_and_string_input.yaml @@ -0,0 +1,107 @@ +interactions: +- request: + body: '{"messages": [{"role": "user", "content": "What is the current year?"}], + "model": "gpt-4o-mini", "tools": [{"type": "function", "function": {"name": + "get_current_year", "description": "Returns the current year as a string.", + "parameters": {"type": "object", "properties": {}, "required": []}}}]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '295' + content-type: + - application/json + cookie: + - _cfuvid=8NrWEBP3dDmc8p2.csR.EdsSwS8zFvzWI1kPICaK_fM-1737568015338-0.0.1.1-604800000; + __cf_bm=pKr3NwXmTZN9rMSlKvEX40VPKbrxF93QwDNHunL2v8Y-1737568015-1.0.1.1-nR0EA7hYIwWpIBYUI53d9xQrUnl5iML6lgz4AGJW4ZGPBDxFma3PZ2cBhlr_hE7wKa5fV3r32eMu_rNWMXD.eA + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.59.6 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.59.6 + x-stainless-raw-response: + - 'true' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.7 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + content: "{\n \"id\": \"chatcmpl-AsZJ8HKXQU9nTB7xbGAkKxqrg9BZ2\",\n \"object\": + \"chat.completion\",\n \"created\": 1737568798,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": null,\n \"tool_calls\": [\n {\n + \ \"id\": \"call_mfvEs2jngeFloVZpZOHZVaKY\",\n \"type\": + \"function\",\n \"function\": {\n \"name\": \"get_current_year\",\n + \ \"arguments\": \"{}\"\n }\n }\n ],\n + \ \"refusal\": null\n },\n \"logprobs\": null,\n \"finish_reason\": + \"tool_calls\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 46,\n \"completion_tokens\": + 12,\n \"total_tokens\": 58,\n \"prompt_tokens_details\": {\n \"cached_tokens\": + 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n + \ \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": + 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": + \"default\",\n \"system_fingerprint\": \"fp_72ed7ab54c\"\n}\n" + headers: + CF-Cache-Status: + - DYNAMIC + CF-RAY: + - 906170e038281775-IAD + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Wed, 22 Jan 2025 17:59:59 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '416' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999975' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_4039a5e5772d1790a3131f0b1ea06139 + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/tests/llm_test.py b/tests/llm_test.py index 551309389..6d1e6a188 100644 --- a/tests/llm_test.py +++ b/tests/llm_test.py @@ -4,6 +4,7 @@ import pytest from crewai.agents.agent_builder.utilities.base_token_process import TokenProcess from crewai.llm import LLM +from crewai.tools import tool from crewai.utilities.token_counter_callback import TokenCalcHandler @@ -37,3 +38,119 @@ def test_llm_callback_replacement(): assert usage_metrics_1.successful_requests == 1 assert usage_metrics_2.successful_requests == 1 assert usage_metrics_1 == calc_handler_1.token_cost_process.get_summary() + + +@pytest.mark.vcr(filter_headers=["authorization"]) +def test_llm_call_with_string_input(): + llm = LLM(model="gpt-4o-mini") + + # Test the call method with a string input + result = llm.call("Return the name of a random city in the world.") + assert isinstance(result, str) + assert len(result.strip()) > 0 # Ensure the response is not empty + + +@pytest.mark.vcr(filter_headers=["authorization"]) +def test_llm_call_with_string_input_and_callbacks(): + llm = LLM(model="gpt-4o-mini") + calc_handler = TokenCalcHandler(token_cost_process=TokenProcess()) + + # Test the call method with a string input and callbacks + result = llm.call( + "Tell me a joke.", + callbacks=[calc_handler], + ) + usage_metrics = calc_handler.token_cost_process.get_summary() + + assert isinstance(result, str) + assert len(result.strip()) > 0 + assert usage_metrics.successful_requests == 1 + + +@pytest.mark.vcr(filter_headers=["authorization"]) +def test_llm_call_with_message_list(): + llm = LLM(model="gpt-4o-mini") + messages = [{"role": "user", "content": "What is the capital of France?"}] + + # Test the call method with a list of messages + result = llm.call(messages) + assert isinstance(result, str) + assert "Paris" in result + + +@pytest.mark.vcr(filter_headers=["authorization"]) +def test_llm_call_with_tool_and_string_input(): + llm = LLM(model="gpt-4o-mini") + + def get_current_year() -> str: + """Returns the current year as a string.""" + from datetime import datetime + + return str(datetime.now().year) + + # Create tool schema + tool_schema = { + "type": "function", + "function": { + "name": "get_current_year", + "description": "Returns the current year as a string.", + "parameters": { + "type": "object", + "properties": {}, + "required": [], + }, + }, + } + + # Available functions mapping + available_functions = {"get_current_year": get_current_year} + + # Test the call method with a string input and tool + result = llm.call( + "What is the current year?", + tools=[tool_schema], + available_functions=available_functions, + ) + + assert isinstance(result, str) + assert result == get_current_year() + + +@pytest.mark.vcr(filter_headers=["authorization"]) +def test_llm_call_with_tool_and_message_list(): + llm = LLM(model="gpt-4o-mini") + + def square_number(number: int) -> int: + """Returns the square of a number.""" + return number * number + + # Create tool schema + tool_schema = { + "type": "function", + "function": { + "name": "square_number", + "description": "Returns the square of a number.", + "parameters": { + "type": "object", + "properties": { + "number": {"type": "integer", "description": "The number to square"} + }, + "required": ["number"], + }, + }, + } + + # Available functions mapping + available_functions = {"square_number": square_number} + + messages = [{"role": "user", "content": "What is the square of 5?"}] + + # Test the call method with messages and tool + result = llm.call( + messages, + tools=[tool_schema], + available_functions=available_functions, + ) + + assert isinstance(result, int) + assert result == 25