From eab4959ceea1b2320629184f232d4ac89c845559 Mon Sep 17 00:00:00 2001 From: Zishan Date: Tue, 4 Feb 2025 14:35:25 +0100 Subject: [PATCH 1/8] init anthropic policy --- README.md | 5 +++++ proxy/routes/anthropic.py | 40 ++++++++++++++++++++++++++++++++++++++- proxy/routes/open_ai.py | 2 ++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 75e581f..2e39748 100644 --- a/README.md +++ b/README.md @@ -34,3 +34,8 @@ To integrate Explorer Proxy with your AI agent, you’ll need to modify how your ### **🔹 Anthropic Integration** Coming Soon! + +### Run +docker compose -f docker-compose.local.yml down +&& docker compose -f docker-compose.local.yml up -d --build +&& docker logs explorer-proxy-explorer-proxy-1 -f \ No newline at end of file diff --git a/proxy/routes/anthropic.py b/proxy/routes/anthropic.py index 6e1f61b..544a6cb 100644 --- a/proxy/routes/anthropic.py +++ b/proxy/routes/anthropic.py @@ -1,5 +1,43 @@ """Proxy service to forward requests to the Anthropic APIs""" -from fastapi import APIRouter +from fastapi import APIRouter, Header, HTTPException, Depends, Request proxy = APIRouter() + +ALLOWED_ANTHROPIC_ENDPOINTS = {"v1/messages"} +MISSING_INVARIANT_AUTH_HEADER = "Missing invariant-authorization header" +MISSING_AUTH_HEADER = "Missing authorization header" +NOT_SUPPORTED_ENDPOINT = "Not supported OpenAI endpoint" +FAILED_TO_PUSH_TRACE = "Failed to push trace to the dataset: " + + +def validate_headers( + invariant_authorization: str = Header(None), authorization: str = Header(None) +): + """Require the invariant-authorization and authorization headers to be present""" + if invariant_authorization is None: + raise HTTPException(status_code=400, detail=MISSING_INVARIANT_AUTH_HEADER) + # if authorization is None: + # raise HTTPException(status_code=400, detail=MISSING_AUTH_HEADER) + +@proxy.post( + "/{dataset_name}/anthropic/{endpoint:path}", + dependencies=[Depends(validate_headers)], +) +async def anthropic_proxy( + dataset_name: str, + endpoint: str, + request: Request, +): + """Proxy calls to the Anthropic APIs""" + if endpoint not in ALLOWED_ANTHROPIC_ENDPOINTS: + raise HTTPException(status_code=404, detail=NOT_SUPPORTED_ENDPOINT) + + headers = { + k: v for k, v in request.headers.items() + } + headers["accept-encoding"] = "identity" + + request_body = await request.body() + + print("request_body", request_body) diff --git a/proxy/routes/open_ai.py b/proxy/routes/open_ai.py index 883bdfc..76b061d 100644 --- a/proxy/routes/open_ai.py +++ b/proxy/routes/open_ai.py @@ -56,6 +56,8 @@ async def openai_proxy( request_body = await request.body() + print("request_body", request_body) + async with httpx.AsyncClient() as client: open_ai_request = client.build_request( "POST", From ff80bf462cb010e7b227d3e07dba72a994cb4370 Mon Sep 17 00:00:00 2001 From: Zishan Date: Wed, 5 Feb 2025 15:36:52 +0100 Subject: [PATCH 2/8] add anthropic policy --- proxy/routes/anthropic.py | 145 ++++++++++++++++++++++++++++++++++++-- proxy/routes/open_ai.py | 1 - 2 files changed, 141 insertions(+), 5 deletions(-) diff --git a/proxy/routes/anthropic.py b/proxy/routes/anthropic.py index 544a6cb..cbb5e2b 100644 --- a/proxy/routes/anthropic.py +++ b/proxy/routes/anthropic.py @@ -1,15 +1,36 @@ """Proxy service to forward requests to the Anthropic APIs""" from fastapi import APIRouter, Header, HTTPException, Depends, Request +import json +import httpx +from typing import Any +from utils.explorer import push_trace +# from .open_ai import push_to_explorer proxy = APIRouter() ALLOWED_ANTHROPIC_ENDPOINTS = {"v1/messages"} +IGNORED_HEADERS = [ + "accept-encoding", + "host", + "invariant-authorization", + "x-forwarded-for", + "x-forwarded-host", + "x-forwarded-port", + "x-forwarded-proto", + "x-forwarded-server", + "x-real-ip", +] + MISSING_INVARIANT_AUTH_HEADER = "Missing invariant-authorization header" MISSING_AUTH_HEADER = "Missing authorization header" NOT_SUPPORTED_ENDPOINT = "Not supported OpenAI endpoint" FAILED_TO_PUSH_TRACE = "Failed to push trace to the dataset: " - +END_REASONS = [ + "end_turn", + "max_tokens", + "stop_sequence" +] def validate_headers( invariant_authorization: str = Header(None), authorization: str = Header(None) @@ -34,10 +55,126 @@ async def anthropic_proxy( raise HTTPException(status_code=404, detail=NOT_SUPPORTED_ENDPOINT) headers = { - k: v for k, v in request.headers.items() + k: v for k, v in request.headers.items() if k.lower() not in IGNORED_HEADERS } - headers["accept-encoding"] = "identity" + # headers["accept-encoding"] = "identity" request_body = await request.body() - print("request_body", request_body) + request_body_json = json.loads(request_body) + + anthropic_url = f"https://api.anthropic.com/{endpoint}" + client = httpx.AsyncClient() + + anthropic_request = client.build_request( + "POST", + anthropic_url, + headers=headers, + data=request_body + ) + + invariant_authorization = request.headers.get("invariant-authorization") + + async with client: + response = await client.send(anthropic_request) + await handle_non_streaming_response( + response, dataset_name, request_body_json, invariant_authorization + ) + return response.json() + +async def push_to_explorer( + dataset_name: str, + merged_response: dict[str, Any], + request_body: dict[str, Any], + invariant_authorization: str, +) -> None: + """Pushes the full trace to the Invariant Explorer""" + # Combine the messages from the request body and the choices from the OpenAI response + messages = request_body.get("messages", []) + if merged_response is not list: + merged_response = [merged_response] + messages += merged_response + if messages[-1].get("stop_reason") in END_REASONS: + messages = anthropic_to_invariant_messages(messages) + response = await push_trace( + dataset_name=dataset_name, + messages=[messages], + invariant_authorization=invariant_authorization, + ) + +async def handle_non_streaming_response( + response: httpx.Response, + dataset_name: str, + request_body_json: dict[str, Any], + invariant_authorization: str, +): + """Handles non-streaming Anthropic responses""" + json_response = response.json() + await push_to_explorer( + dataset_name, + json_response, + request_body_json, + invariant_authorization, + ) + +def anthropic_to_invariant_messages( + messages: list[dict], keep_empty_tool_response: bool = False +) -> list[dict]: + """Converts a list of messages from the Anthropic API to the Invariant API format.""" + output = [] + for message in messages: + if message["role"] == "system": + output.append({"role": "system", "content": message["content"]}) + if message["role"] == "user": + if isinstance(message["content"], list): + for sub_message in message["content"]: + if sub_message["type"] == "tool_result": + if sub_message["content"]: + output.append( + { + "role": "tool", + "content": sub_message["content"], + "tool_id": sub_message["tool_use_id"], + } + ) + else: + if keep_empty_tool_response and any( + [sub_message[k] for k in sub_message] + ): + output.append( + { + "role": "tool", + "content": {"is_error": True} + if sub_message["is_error"] + else {}, + "tool_id": sub_message["tool_use_id"], + } + ) + elif sub_message["type"] == "text": + output.append({"role": "user", "content": sub_message["text"]}) + else: + output.append({"role": "user", "content": message["content"]}) + if message["role"] == "assistant": + for sub_message in message["content"]: + if sub_message["type"] == "text": + output.append( + {"role": "assistant", "content": sub_message.get("text")} + ) + if sub_message["type"] == "tool_use": + output.append( + { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "tool_id": sub_message.get("id"), + "type": "function", + "function": { + "name": sub_message.get("name"), + "arguments": sub_message.get("input"), + }, + } + ], + } + ) + return output \ No newline at end of file diff --git a/proxy/routes/open_ai.py b/proxy/routes/open_ai.py index 34f709d..74fbb44 100644 --- a/proxy/routes/open_ai.py +++ b/proxy/routes/open_ai.py @@ -300,7 +300,6 @@ async def push_to_explorer( # Combine the messages from the request body and the choices from the OpenAI response messages = request_body.get("messages", []) messages += [choice["message"] for choice in merged_response.get("choices", [])] - _ = await push_trace( dataset_name=dataset_name, messages=[messages], From 2f2253220e747a05cefeb10ba12359246e9d6b45 Mon Sep 17 00:00:00 2001 From: Zishan Date: Wed, 5 Feb 2025 15:49:11 +0100 Subject: [PATCH 3/8] add test claude agent --- proxy/__pycache__/serve.cpython-310.pyc | Bin 639 -> 766 bytes proxy/requirements.txt | 4 +- .../__pycache__/anthropic.cpython-310.pyc | Bin 1627 -> 4327 bytes .../__pycache__/open_ai.cpython-310.pyc | Bin 7222 -> 7222 bytes proxy/tests/claude_weather_agent | 117 ++++++++++++++++++ 5 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 proxy/tests/claude_weather_agent diff --git a/proxy/__pycache__/serve.cpython-310.pyc b/proxy/__pycache__/serve.cpython-310.pyc index d3f6587dc652ee6474b56018b61964ad80bdd0f6..ca4e2fc86ceae7f7c5e328409e6d88885a6e4286 100644 GIT binary patch delta 333 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 diff --git a/proxy/routes/__pycache__/open_ai.cpython-310.pyc b/proxy/routes/__pycache__/open_ai.cpython-310.pyc index f486d96b5f7100f890610c22eddc7b06c7ccc950..ec37170683a2d880d9807c3e0fc61663a128bff4 100644 GIT binary patch delta 841 zcmZ9K!EX{#5XOBAT`A213xUGgsxc}dsY#_!K^tw2##kz%)^b1;B(}MfNZ|l*^ zjgxP*cLC=0J_Qc4NKqvDJa7ug_kN8Reaxe*_KX$iy_Skg(SR7*wb6I&%lX|hDd+!t zt|N*A*MJ0&6{w1@qY5Zbj8Le+v!sSi^)%!&bT3?quOh4fE5H??-1{7!VC*t|jT9rJ z8m09awoI$~33VCfYVWDO#PSP>&jROwMFDTjX<7CrXNdj0?b_bB+37f)uG#9AGd^CU zLDap35%)HSTWvJkPQ&Ik{0ZBeqQ+@$)OWIXP$4fXAuqc|uVZtOH7M(V^q9RLu`M=r z8;P3&Ua;G4yY+_Yx*d5oU!V{1qemPV?h0i5J878x-N*NB-h@`CU}Acy0f)Bd{4z8X zI4&Rx#Oc*x4|#QOzwGB&eAt1SiEO+M|H$+fy-L)WMH7?XqH|VC k^Es_9ms=wr%7P#V%F%@b+qy1h{HrL+pP)|}DE`pze_iXhApigX delta 841 zcmZ9KO-~b16o$RiVJs!n&$P57s4;5O1QTe%wh}cM5~vcuj|7+kb!-QiICji!7r1k0 z;?naA^be?M>_*+WFfQHMg$Z%#QWq?&df!U}q?62(Gv|HJJ@?)@?Lxbtd%7MtAs%1y z)pqu+{=v`8)))V27A}Y+p8_reldb*1qK}!B)t|FGebiHNDH;%iM>hJYf4gy1CgsUP z&oZJoummK42?0~Q4NX9KY6OD?K0#W@(u$C;(4)|D{0_oJ;3jYjSZI9>4KX%H-^0al zzeefE7@MbikxSY<%&V$let-PJO%5%Wj}T9#%pgc7@)=a^V#yRX}<+TfbuO z*hm?Py8>pQ>3MFYYPoJhuFa?EQ~dln8-^``jM__!xT7|oJhpia+ByXiqciv6(2uRZ z0BsYvC}65sz1Kq?-8?D#IW>OJfp!wv_&WT3ySM0dqRcEB8vYTLyM>-n_!)Q~yhtwV zxKMFr^0&LRksM9SD|-looDx?=9aiaIxkN9Mv6!MTl_@OT Dict: + """ + Parse user query to extract weather-related parameters using Claude. + """ + messages = [ + { + "role": "user", + "content": user_query + } + ] + while True: + response = self.client.messages.create( + # system=self.system_prompt, + tools = [self.example_function], + model="claude-3-5-sonnet-20241022", + max_tokens=1024, + messages=messages + ) + print("response content:",response.content[0].text) + + # If there's tool call, Extract the tool call parameters from the response + if len(response.content) > 1 and response.content[1].type == "tool_use": + print("response tools:",response.content[1].input) + tool_call_params = response.content[1].input + tool_call_result = self.get_weather(tool_call_params["location"]) + tool_call_id = response.content[1].id + messages.append({ + "role": response.role, + "content": response.content + } + ) + messages.append({ + "role": "user", + "content": [{ + "type": "tool_result", + "tool_use_id": tool_call_id, + "content": tool_call_result + }] + }) + print("messages:",messages,type(messages)) + else: + return response.content[0].text + + def get_weather(self, location: str): + """Get the current weather in a given location using latitude and longitude.""" + query = f"What is the weather in {location}?" + response = tavily_client.search(query) + # breakpoint() + response_content = response["results"][0]["content"] + return response["results"][0]["title"] + ":\n" + response_content + +# Example usage +def main(): + # Initialize agent with your Anthropic API key + api_key = os.getenv("ANTHROPIC_API_KEY") + weather_agent = WeatherAgent(api_key) + + # Example queries + queries = [ + "What's the weather like in Zurich city?", + "Tell me the forecast for New York", + "How's the weather in London next week?" + ] + + # Process each query + for query in queries: + print(f"\nQuery: {query}") + response = weather_agent.parse_weather_query(query) + print(f"Response: {response}") + +if __name__ == "__main__": + main() \ No newline at end of file From f2ffed91d3434b606318b671e184e33570fec207 Mon Sep 17 00:00:00 2001 From: Zishan Date: Thu, 6 Feb 2025 14:46:04 +0100 Subject: [PATCH 4/8] add test for anthropic agent --- proxy/routes/anthropic.py | 126 ++++++++++-------- ...her_agent => test_claude_weather_agent.py} | 51 ++++--- 2 files changed, 91 insertions(+), 86 deletions(-) rename proxy/tests/{claude_weather_agent => test_claude_weather_agent.py} (72%) diff --git a/proxy/routes/anthropic.py b/proxy/routes/anthropic.py index cbb5e2b..f209663 100644 --- a/proxy/routes/anthropic.py +++ b/proxy/routes/anthropic.py @@ -23,7 +23,7 @@ IGNORED_HEADERS = [ ] MISSING_INVARIANT_AUTH_HEADER = "Missing invariant-authorization header" -MISSING_AUTH_HEADER = "Missing authorization header" +MISSING_ANTHROPIC_AUTH_HEADER = "Missing athropic authorization header" NOT_SUPPORTED_ENDPOINT = "Not supported OpenAI endpoint" FAILED_TO_PUSH_TRACE = "Failed to push trace to the dataset: " END_REASONS = [ @@ -33,13 +33,13 @@ END_REASONS = [ ] def validate_headers( - invariant_authorization: str = Header(None), authorization: str = Header(None) + invariant_authorization: str = Header(None), x_api_key: str = Header(None) ): """Require the invariant-authorization and authorization headers to be present""" if invariant_authorization is None: raise HTTPException(status_code=400, detail=MISSING_INVARIANT_AUTH_HEADER) - # if authorization is None: - # raise HTTPException(status_code=400, detail=MISSING_AUTH_HEADER) + if x_api_key is None: + raise HTTPException(status_code=400, detail=MISSING_ANTHROPIC_AUTH_HEADER) @proxy.post( "/{dataset_name}/anthropic/{endpoint:path}", @@ -53,11 +53,9 @@ async def anthropic_proxy( """Proxy calls to the Anthropic APIs""" if endpoint not in ALLOWED_ANTHROPIC_ENDPOINTS: raise HTTPException(status_code=404, detail=NOT_SUPPORTED_ENDPOINT) - headers = { k: v for k, v in request.headers.items() if k.lower() not in IGNORED_HEADERS } - # headers["accept-encoding"] = "identity" request_body = await request.body() @@ -89,14 +87,16 @@ async def push_to_explorer( invariant_authorization: str, ) -> None: """Pushes the full trace to the Invariant Explorer""" - # Combine the messages from the request body and the choices from the OpenAI response + # Combine the messages from the request body and Anthropic response messages = request_body.get("messages", []) if merged_response is not list: merged_response = [merged_response] messages += merged_response + + # Only push the trace to explorer if the last message is an end turn message if messages[-1].get("stop_reason") in END_REASONS: messages = anthropic_to_invariant_messages(messages) - response = await push_trace( + _ = await push_trace( dataset_name=dataset_name, messages=[messages], invariant_authorization=invariant_authorization, @@ -122,59 +122,67 @@ def anthropic_to_invariant_messages( ) -> list[dict]: """Converts a list of messages from the Anthropic API to the Invariant API format.""" output = [] + role_mapping = { + "system": lambda msg: {"role": "system", "content": msg["content"]}, + "user": lambda msg: handle_user_message(msg, keep_empty_tool_response), + "assistant": lambda msg: handle_assistant_message(msg), + } + for message in messages: - if message["role"] == "system": - output.append({"role": "system", "content": message["content"]}) - if message["role"] == "user": - if isinstance(message["content"], list): - for sub_message in message["content"]: - if sub_message["type"] == "tool_result": - if sub_message["content"]: - output.append( - { - "role": "tool", - "content": sub_message["content"], - "tool_id": sub_message["tool_use_id"], - } - ) - else: - if keep_empty_tool_response and any( - [sub_message[k] for k in sub_message] - ): - output.append( - { - "role": "tool", - "content": {"is_error": True} - if sub_message["is_error"] - else {}, - "tool_id": sub_message["tool_use_id"], - } - ) - elif sub_message["type"] == "text": - output.append({"role": "user", "content": sub_message["text"]}) - else: - output.append({"role": "user", "content": message["content"]}) - if message["role"] == "assistant": - for sub_message in message["content"]: - if sub_message["type"] == "text": - output.append( - {"role": "assistant", "content": sub_message.get("text")} - ) - if sub_message["type"] == "tool_use": + handler = role_mapping.get(message["role"]) + if handler: + output.extend(handler(message)) + + return output + +def handle_user_message(message, keep_empty_tool_response): + output = [] + content = message["content"] + if isinstance(content, list): + for sub_message in content: + if sub_message["type"] == "tool_result": + if sub_message["content"]: output.append( { - "role": "assistant", - "content": None, - "tool_calls": [ - { - "tool_id": sub_message.get("id"), - "type": "function", - "function": { - "name": sub_message.get("name"), - "arguments": sub_message.get("input"), - }, - } - ], + "role": "tool", + "content": sub_message["content"], + "tool_id": sub_message["tool_use_id"], } ) - return output \ No newline at end of file + elif keep_empty_tool_response and any(sub_message.values()): + output.append( + { + "role": "tool", + "content": {"is_error": True} if sub_message["is_error"] else {}, + "tool_id": sub_message["tool_use_id"], + } + ) + elif sub_message["type"] == "text": + output.append({"role": "user", "content": sub_message["text"]}) + else: + output.append({"role": "user", "content": content}) + return output + +def handle_assistant_message(message): + output = [] + for sub_message in message["content"]: + if sub_message["type"] == "text": + output.append({"role": "assistant", "content": sub_message.get("text")}) + elif sub_message["type"] == "tool_use": + output.append( + { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "tool_id": sub_message.get("id"), + "type": "function", + "function": { + "name": sub_message.get("name"), + "arguments": sub_message.get("input"), + }, + } + ], + } + ) + return output diff --git a/proxy/tests/claude_weather_agent b/proxy/tests/test_claude_weather_agent.py similarity index 72% rename from proxy/tests/claude_weather_agent rename to proxy/tests/test_claude_weather_agent.py index ba26e1a..bacdd54 100644 --- a/proxy/tests/claude_weather_agent +++ b/proxy/tests/test_claude_weather_agent.py @@ -1,24 +1,27 @@ -from anthropic import Anthropic +import anthropic from typing import Dict, Optional, List import os from tavily import TavilyClient import anthropic from httpx import Client +import os +import pytest +# from invariant import testing tavily_client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY")) class WeatherAgent: def __init__(self, api_key: str): - # self.client = Anthropic(api_key=api_key) dataset_name = "claude_weather_agent_test7" - self.client = anthropic.Anthropic( - http_client=Client( - headers={ - "Invariant-Authorization": "Bearer inv-ff9cb8955c73e3d0afef86a5cef1ee773b1b349d9ed40886c78ef99b8d3dbc5a" - }, + invariant_api_key = os.environ.get("INVARIANT_API_KEY") + self.client = anthropic.Anthropic( + http_client=Client( + headers={ + "Invariant-Authorization": f"Bearer {invariant_api_key}" + }, ), base_url=f"http://localhost/api/v1/proxy/{dataset_name}/anthropic", ) - self.example_function = { + self.get_weather_function = { "name": "get_weather", "description": "Get the current weather in a given location", "input_schema": { @@ -38,14 +41,13 @@ class WeatherAgent: } } + # self.system_prompt = """You are an assistant that can perform weather searches using function calls. + # When a user asks for weather information, respond with a JSON object specifying + # the function name `get_weather` and the arguments latitude and longitude are needed.""" - self.system_prompt = """You are an assistant that can perform weather searches using function calls. - When a user asks for weather information, respond with a JSON object specifying - the function name `get_weather` and the arguments latitude and longitude are needed.""" - - def parse_weather_query(self, user_query: str) -> Dict: + def get_response(self, user_query: str) -> Dict: """ - Parse user query to extract weather-related parameters using Claude. + Get the response from the agent for a given user query for weather. """ messages = [ { @@ -56,7 +58,7 @@ class WeatherAgent: while True: response = self.client.messages.create( # system=self.system_prompt, - tools = [self.example_function], + tools = [self.get_weather_function], model="claude-3-5-sonnet-20241022", max_tokens=1024, messages=messages @@ -82,7 +84,6 @@ class WeatherAgent: "content": tool_call_result }] }) - print("messages:",messages,type(messages)) else: return response.content[0].text @@ -90,16 +91,15 @@ class WeatherAgent: """Get the current weather in a given location using latitude and longitude.""" query = f"What is the weather in {location}?" response = tavily_client.search(query) - # breakpoint() response_content = response["results"][0]["content"] return response["results"][0]["title"] + ":\n" + response_content -# Example usage -def main(): - # Initialize agent with your Anthropic API key - api_key = os.getenv("ANTHROPIC_API_KEY") - weather_agent = WeatherAgent(api_key) + +# Initialize agent with your Anthropic API key +anthropic_api_key = os.getenv("ANTHROPIC_API_KEY") +weather_agent = WeatherAgent(anthropic_api_key) +def test_weather_agent(): # Example queries queries = [ "What's the weather like in Zurich city?", @@ -109,9 +109,6 @@ def main(): # Process each query for query in queries: - print(f"\nQuery: {query}") - response = weather_agent.parse_weather_query(query) + response = weather_agent.get_response(query) print(f"Response: {response}") - -if __name__ == "__main__": - main() \ No newline at end of file + assert response is not None \ No newline at end of file From bb810663566bbc74755728d2f52a7f6669eac4dd Mon Sep 17 00:00:00 2001 From: Zishan Date: Thu, 6 Feb 2025 14:57:40 +0100 Subject: [PATCH 5/8] rename anthropic test --- {proxy/tests => tests/anthropic}/test_claude_weather_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename {proxy/tests => tests/anthropic}/test_claude_weather_agent.py (99%) diff --git a/proxy/tests/test_claude_weather_agent.py b/tests/anthropic/test_claude_weather_agent.py similarity index 99% rename from proxy/tests/test_claude_weather_agent.py rename to tests/anthropic/test_claude_weather_agent.py index bacdd54..d740437 100644 --- a/proxy/tests/test_claude_weather_agent.py +++ b/tests/anthropic/test_claude_weather_agent.py @@ -99,7 +99,7 @@ class WeatherAgent: anthropic_api_key = os.getenv("ANTHROPIC_API_KEY") weather_agent = WeatherAgent(anthropic_api_key) -def test_weather_agent(): +def test_proxy_response(): # Example queries queries = [ "What's the weather like in Zurich city?", From bdf27877869e67791c2f7c00576af853ebe9847d Mon Sep 17 00:00:00 2001 From: Zishan Date: Thu, 6 Feb 2025 15:03:32 +0100 Subject: [PATCH 6/8] remove cache file --- proxy/__pycache__/serve.cpython-310.pyc | Bin 766 -> 0 bytes .../routes/__pycache__/__init__.cpython-310.pyc | Bin 122 -> 0 bytes .../routes/__pycache__/anthropic.cpython-310.pyc | Bin 4327 -> 0 bytes proxy/routes/__pycache__/open_ai.cpython-310.pyc | Bin 7222 -> 0 bytes proxy/utils/__pycache__/__init__.cpython-310.pyc | Bin 121 -> 0 bytes proxy/utils/__pycache__/explorer.cpython-310.pyc | Bin 2396 -> 0 bytes 6 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 proxy/__pycache__/serve.cpython-310.pyc delete mode 100644 proxy/routes/__pycache__/__init__.cpython-310.pyc delete mode 100644 proxy/routes/__pycache__/anthropic.cpython-310.pyc delete mode 100644 proxy/routes/__pycache__/open_ai.cpython-310.pyc delete mode 100644 proxy/utils/__pycache__/__init__.cpython-310.pyc delete mode 100644 proxy/utils/__pycache__/explorer.cpython-310.pyc diff --git a/proxy/__pycache__/serve.cpython-310.pyc b/proxy/__pycache__/serve.cpython-310.pyc deleted file mode 100644 index ca4e2fc86ceae7f7c5e328409e6d88885a6e4286..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 766 zcmY*X&5qMB5VoEFrb)XgCob#>sh1Y+2rWWF;s8SI0m&CDs=eKmB#!K)yPJ3DJ1pYF zl^d_(D<@t73BfpxD%K<0GxN=iKaVYwWP)J)xcRy@1fieS_m>+!&oo;>mWOr%5RoX`0U-7noDG&9~aj^18iNvMDx4)P#&xc~LXg2hUc66c~g1l_*aNLAChl3gqG>Qq|6e?ZE&r=}w|Qd0I;&Me}``Dl+))E~+!niln@?6_b@+uo+mE5Vp?R9AmPYG#?(<@aK( y)qZ>yZ69{k+?nu2C3#nhml#6yQ&?OSk`PY^aS6dzrhg{hlw=?i5;~8t`}ZHN)7Yp0 diff --git a/proxy/routes/__pycache__/__init__.cpython-310.pyc b/proxy/routes/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 8ec48f752d5750d48060d05720b72d100f34d6b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 122 zcmd1j<>g`kf`q3F(n0iN5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!HlesNKmenC-w rMWudGerZW+v3`7fW?p7Ve7s&k{?B+q+O-1ScI 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 From 6eecb1510a1f414926d5b88fe2b1f669f622f96c Mon Sep 17 00:00:00 2001 From: Zishan Date: Thu, 6 Feb 2025 15:12:20 +0100 Subject: [PATCH 7/8] adjust anthropic test --- tests/anthropic/test_claude_weather_agent.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/anthropic/test_claude_weather_agent.py b/tests/anthropic/test_claude_weather_agent.py index d740437..299001c 100644 --- a/tests/anthropic/test_claude_weather_agent.py +++ b/tests/anthropic/test_claude_weather_agent.py @@ -1,11 +1,10 @@ import anthropic -from typing import Dict, Optional, List +from typing import Dict import os from tavily import TavilyClient import anthropic from httpx import Client import os -import pytest # from invariant import testing tavily_client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY")) @@ -63,11 +62,9 @@ class WeatherAgent: max_tokens=1024, messages=messages ) - print("response content:",response.content[0].text) # If there's tool call, Extract the tool call parameters from the response if len(response.content) > 1 and response.content[1].type == "tool_use": - print("response tools:",response.content[1].input) tool_call_params = response.content[1].input tool_call_result = self.get_weather(tool_call_params["location"]) tool_call_id = response.content[1].id @@ -106,9 +103,9 @@ def test_proxy_response(): "Tell me the forecast for New York", "How's the weather in London next week?" ] - + cities = ["Zurich", "New York", "London"] # Process each query - for query in queries: + for index,query in enumerate(queries): response = weather_agent.get_response(query) - print(f"Response: {response}") - assert response is not None \ No newline at end of file + assert response is not None + assert cities[index] in response \ No newline at end of file From 87fc58e4de4d62569760d4d45a9ecb05a69df876 Mon Sep 17 00:00:00 2001 From: Zishan Date: Fri, 7 Feb 2025 10:16:16 +0100 Subject: [PATCH 8/8] minor logic fix --- README.md | 4 +-- proxy/routes/anthropic.py | 32 +++++++++----------- tests/anthropic/test_claude_weather_agent.py | 3 +- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 16bb165..1cec817 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,4 @@ To integrate the Proxy with your AI agent, you’ll need to modify how your clie Coming Soon! ### Run -docker compose -f docker-compose.local.yml down -&& docker compose -f docker-compose.local.yml up -d --build -&& docker logs explorer-proxy-explorer-proxy-1 -f \ No newline at end of file +./run.sh up \ No newline at end of file diff --git a/proxy/routes/anthropic.py b/proxy/routes/anthropic.py index f209663..86706f9 100644 --- a/proxy/routes/anthropic.py +++ b/proxy/routes/anthropic.py @@ -89,18 +89,14 @@ async def push_to_explorer( """Pushes the full trace to the Invariant Explorer""" # Combine the messages from the request body and Anthropic response messages = request_body.get("messages", []) - if merged_response is not list: - merged_response = [merged_response] - messages += merged_response + messages += [merged_response] - # Only push the trace to explorer if the last message is an end turn message - if messages[-1].get("stop_reason") in END_REASONS: - messages = anthropic_to_invariant_messages(messages) - _ = await push_trace( - dataset_name=dataset_name, - messages=[messages], - invariant_authorization=invariant_authorization, - ) + messages = anthropic_to_invariant_messages(messages) + _ = await push_trace( + dataset_name=dataset_name, + messages=[messages], + invariant_authorization=invariant_authorization, + ) async def handle_non_streaming_response( response: httpx.Response, @@ -110,12 +106,14 @@ async def handle_non_streaming_response( ): """Handles non-streaming Anthropic responses""" json_response = response.json() - await push_to_explorer( - dataset_name, - json_response, - request_body_json, - invariant_authorization, - ) + # Only push the trace to explorer if the last message is an end turn message + if json_response.get("stop_reason") in END_REASONS: + await push_to_explorer( + dataset_name, + json_response, + request_body_json, + invariant_authorization, + ) def anthropic_to_invariant_messages( messages: list[dict], keep_empty_tool_response: bool = False diff --git a/tests/anthropic/test_claude_weather_agent.py b/tests/anthropic/test_claude_weather_agent.py index 299001c..bb61c3c 100644 --- a/tests/anthropic/test_claude_weather_agent.py +++ b/tests/anthropic/test_claude_weather_agent.py @@ -7,10 +7,11 @@ from httpx import Client import os # from invariant import testing tavily_client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY")) +import datetime class WeatherAgent: def __init__(self, api_key: str): - dataset_name = "claude_weather_agent_test7" + dataset_name = "claude_weather_agent_test" + str(datetime.datetime.now().strftime("%Y%m%d%H%M%S")) invariant_api_key = os.environ.get("INVARIANT_API_KEY") self.client = anthropic.Anthropic( http_client=Client(