From 3f7b7581f4d650aa53cc44b1fc5176ce3d9dc8af Mon Sep 17 00:00:00 2001 From: Sathvik Ravi Date: Sat, 5 Oct 2024 14:44:36 -0700 Subject: [PATCH] Changed ollama version to incorporate tool calling --- .../instrumentation/ollama/__init__.py | 59 +++++++++++++++++++ .../pyproject.toml | 2 +- .../test_ollama_chat_tool_calls.yaml | 45 ++++++++++++++ .../tests/test_chat.py | 48 +++++++++++++++ 4 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 packages/opentelemetry-instrumentation-ollama/tests/cassettes/test_chat/test_ollama_chat_tool_calls.yaml diff --git a/packages/opentelemetry-instrumentation-ollama/opentelemetry/instrumentation/ollama/__init__.py b/packages/opentelemetry-instrumentation-ollama/opentelemetry/instrumentation/ollama/__init__.py index ef23ab711..488eed138 100644 --- a/packages/opentelemetry-instrumentation-ollama/opentelemetry/instrumentation/ollama/__init__.py +++ b/packages/opentelemetry-instrumentation-ollama/opentelemetry/instrumentation/ollama/__init__.py @@ -2,6 +2,7 @@ import logging import os +import json from typing import Collection from opentelemetry.instrumentation.ollama.config import Config from opentelemetry.instrumentation.ollama.utils import dont_throw @@ -57,6 +58,61 @@ def _set_span_attribute(span, name, value): return +def _set_prompts(span, messages): + if not span.is_recording() or messages is None: + return + for i, msg in enumerate(messages): + prefix = f"{SpanAttributes.LLM_PROMPTS}.{i}" + + _set_span_attribute(span, f"{prefix}.role", msg.get("role")) + if msg.get("content"): + content = msg.get("content") + if isinstance(content, list): + content = json.dumps(content) + _set_span_attribute(span, f"{prefix}.content", content) + if msg.get("tool_call_id"): + _set_span_attribute(span, f"{prefix}.tool_call_id", msg.get("tool_call_id")) + tool_calls = msg.get("tool_calls") + if tool_calls: + for i, tool_call in enumerate(tool_calls): + function = tool_call.get("function") + _set_span_attribute( + span, + f"{prefix}.tool_calls.{i}.id", + tool_call.get("id"), + ) + _set_span_attribute( + span, + f"{prefix}.tool_calls.{i}.name", + function.get("name"), + ) + _set_span_attribute( + span, + f"{prefix}.tool_calls.{i}.arguments", + function.get("arguments"), + ) + + if function.get("arguments"): + function["arguments"] = json.loads(function.get("arguments")) + + +def set_tools_attributes(span, tools): + if not tools: + return + + for i, tool in enumerate(tools): + function = tool.get("function") + if not function: + continue + + prefix = f"{SpanAttributes.LLM_REQUEST_FUNCTIONS}.{i}" + _set_span_attribute(span, f"{prefix}.name", function.get("name")) + _set_span_attribute(span, f"{prefix}.description", function.get("description")) + _set_span_attribute( + span, f"{prefix}.parameters", json.dumps(function.get("parameters")) + ) + + @dont_throw def _set_input_attributes(span, llm_request_type, kwargs): _set_span_attribute(span, SpanAttributes.LLM_REQUEST_MODEL, kwargs.get("model")) @@ -78,6 +134,9 @@ def _set_input_attributes(span, llm_request_type, kwargs): f"{SpanAttributes.LLM_PROMPTS}.{index}.role", message.get("role"), ) + _set_prompts(span, kwargs.get("messages")) + if kwargs.get("tools"): + set_tools_attributes(span, kwargs.get("tools")) else: _set_span_attribute(span, f"{SpanAttributes.LLM_PROMPTS}.0.role", "user") _set_span_attribute( diff --git a/packages/opentelemetry-instrumentation-ollama/pyproject.toml b/packages/opentelemetry-instrumentation-ollama/pyproject.toml index b551bcff4..64b3b6c3f 100644 --- a/packages/opentelemetry-instrumentation-ollama/pyproject.toml +++ b/packages/opentelemetry-instrumentation-ollama/pyproject.toml @@ -28,7 +28,7 @@ opentelemetry-instrumentation = "^0.48b0" opentelemetry-semantic-conventions = "^0.48b0" opentelemetry-semantic-conventions-ai = "0.4.1" -ollama = "^0.2.0" +ollama = "^0.3.2" [tool.poetry.group.dev.dependencies] autopep8 = "^2.2.0" flake8 = "7.0.0" diff --git a/packages/opentelemetry-instrumentation-ollama/tests/cassettes/test_chat/test_ollama_chat_tool_calls.yaml b/packages/opentelemetry-instrumentation-ollama/tests/cassettes/test_chat/test_ollama_chat_tool_calls.yaml new file mode 100644 index 000000000..e5fde7fe8 --- /dev/null +++ b/packages/opentelemetry-instrumentation-ollama/tests/cassettes/test_chat/test_ollama_chat_tool_calls.yaml @@ -0,0 +1,45 @@ +interactions: +- request: + body: '{"model": "llama3.1", "messages": [{"role": "assistant", "content": "", + "tool_calls": [{"function": {"name": "get_current_weather", "arguments": {"location": + "San Francisco"}}}]}, {"role": "tool", "content": "The weather in San Francisco + is 70 degrees and sunny."}], "tools": [], "stream": false, "format": "", "options": + {}, "keep_alive": null}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '345' + content-type: + - application/json + host: + - 127.0.0.1:11434 + user-agent: + - ollama-python/0.3.2 (arm64 darwin) Python/3.12.4 + method: POST + uri: http://127.0.0.1:11434/api/chat + response: + body: + string: '{"model":"llama3.1","created_at":"2024-09-16T02:41:57.128675Z","message":{"role":"assistant","content":"This + is a simple example, but in a real-world scenario, you would need to have + an API key to access the OpenWeatherMap API. You can get one for free on their + website. Also, this code should be placed in a file and run from there instead + of running it directly in the Python interpreter. \n\nNote: This is just a + simple example, in real-world scenarios, you would need to handle errors more + robustly and potentially add more features such as getting weather data for + multiple locations at once or handling different units (e.g., Celsius)."},"done_reason":"stop","done":true,"total_duration":6037504209,"load_duration":28126292,"prompt_eval_count":44,"prompt_eval_duration":182953000,"eval_count":113,"eval_duration":5825102000}' + headers: + Content-Length: + - '830' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 16 Sep 2024 02:41:57 GMT + status: + code: 200 + message: OK +version: 1 diff --git a/packages/opentelemetry-instrumentation-ollama/tests/test_chat.py b/packages/opentelemetry-instrumentation-ollama/tests/test_chat.py index 19270441b..9d827c7f8 100644 --- a/packages/opentelemetry-instrumentation-ollama/tests/test_chat.py +++ b/packages/opentelemetry-instrumentation-ollama/tests/test_chat.py @@ -40,6 +40,54 @@ def test_ollama_chat(exporter): ) +@pytest.mark.vcr +def test_ollama_chat_tool_calls(exporter): + ollama.chat( + model="llama3.1", + messages=[ + { + 'role': 'assistant', + 'content': '', + 'tool_calls': [{ + 'function': { + 'name': 'get_current_weather', + 'arguments': '{"location": "San Francisco"}' + } + }] + }, + { + 'role': 'tool', + 'content': 'The weather in San Francisco is 70 degrees and sunny.' + } + ], + ) + + spans = exporter.get_finished_spans() + ollama_span = spans[0] + + assert ollama_span.name == "ollama.chat" + assert ollama_span.attributes.get(f"{SpanAttributes.LLM_SYSTEM}") == "Ollama" + assert ollama_span.attributes.get(f"{SpanAttributes.LLM_REQUEST_TYPE}") == "chat" + assert not ollama_span.attributes.get(f"{SpanAttributes.LLM_IS_STREAMING}") + assert ollama_span.attributes.get(f"{SpanAttributes.LLM_REQUEST_MODEL}") == "llama3.1" + assert f"{SpanAttributes.LLM_REQUEST_FUNCTIONS}.0.content" not in ollama_span.attributes + assert ( + ollama_span.attributes[f"{SpanAttributes.LLM_PROMPTS}.0.tool_calls.0.name"] + == "get_current_weather" + ) + assert ( + ollama_span.attributes[ + f"{SpanAttributes.LLM_PROMPTS}.0.tool_calls.0.arguments" + ] + == '{"location": "San Francisco"}' + ) + + assert ( + ollama_span.attributes[f"{SpanAttributes.LLM_PROMPTS}.1.content"] + == "The weather in San Francisco is 70 degrees and sunny." + ) + + @pytest.mark.vcr def test_ollama_streaming_chat(exporter): gen = ollama.chat(