ragaai-catalyst 2.1b0__py3-none-any.whl → 2.1b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. ragaai_catalyst/__init__.py +1 -0
  2. ragaai_catalyst/dataset.py +1 -4
  3. ragaai_catalyst/evaluation.py +4 -5
  4. ragaai_catalyst/guard_executor.py +97 -0
  5. ragaai_catalyst/guardrails_manager.py +41 -15
  6. ragaai_catalyst/internal_api_completion.py +1 -1
  7. ragaai_catalyst/prompt_manager.py +7 -2
  8. ragaai_catalyst/ragaai_catalyst.py +1 -1
  9. ragaai_catalyst/synthetic_data_generation.py +7 -0
  10. ragaai_catalyst/tracers/__init__.py +1 -1
  11. ragaai_catalyst/tracers/agentic_tracing/__init__.py +3 -0
  12. ragaai_catalyst/tracers/agentic_tracing/agent_tracer.py +422 -0
  13. ragaai_catalyst/tracers/agentic_tracing/agentic_tracing.py +198 -0
  14. ragaai_catalyst/tracers/agentic_tracing/base.py +376 -0
  15. ragaai_catalyst/tracers/agentic_tracing/data_structure.py +248 -0
  16. ragaai_catalyst/tracers/agentic_tracing/examples/FinancialAnalysisSystem.ipynb +536 -0
  17. ragaai_catalyst/tracers/agentic_tracing/examples/GameActivityEventPlanner.ipynb +134 -0
  18. ragaai_catalyst/tracers/agentic_tracing/examples/TravelPlanner.ipynb +563 -0
  19. ragaai_catalyst/tracers/agentic_tracing/file_name_tracker.py +46 -0
  20. ragaai_catalyst/tracers/agentic_tracing/llm_tracer.py +808 -0
  21. ragaai_catalyst/tracers/agentic_tracing/network_tracer.py +286 -0
  22. ragaai_catalyst/tracers/agentic_tracing/sample.py +197 -0
  23. ragaai_catalyst/tracers/agentic_tracing/tool_tracer.py +247 -0
  24. ragaai_catalyst/tracers/agentic_tracing/unique_decorator.py +165 -0
  25. ragaai_catalyst/tracers/agentic_tracing/unique_decorator_test.py +172 -0
  26. ragaai_catalyst/tracers/agentic_tracing/upload_agentic_traces.py +187 -0
  27. ragaai_catalyst/tracers/agentic_tracing/upload_code.py +115 -0
  28. ragaai_catalyst/tracers/agentic_tracing/user_interaction_tracer.py +43 -0
  29. ragaai_catalyst/tracers/agentic_tracing/utils/__init__.py +3 -0
  30. ragaai_catalyst/tracers/agentic_tracing/utils/api_utils.py +18 -0
  31. ragaai_catalyst/tracers/agentic_tracing/utils/data_classes.py +61 -0
  32. ragaai_catalyst/tracers/agentic_tracing/utils/generic.py +32 -0
  33. ragaai_catalyst/tracers/agentic_tracing/utils/llm_utils.py +177 -0
  34. ragaai_catalyst/tracers/agentic_tracing/utils/model_costs.json +7823 -0
  35. ragaai_catalyst/tracers/agentic_tracing/utils/trace_utils.py +74 -0
  36. ragaai_catalyst/tracers/agentic_tracing/zip_list_of_unique_files.py +342 -0
  37. ragaai_catalyst/tracers/exporters/raga_exporter.py +1 -7
  38. ragaai_catalyst/tracers/tracer.py +30 -4
  39. ragaai_catalyst/tracers/upload_traces.py +127 -0
  40. ragaai_catalyst-2.1b1.dist-info/METADATA +43 -0
  41. ragaai_catalyst-2.1b1.dist-info/RECORD +56 -0
  42. {ragaai_catalyst-2.1b0.dist-info → ragaai_catalyst-2.1b1.dist-info}/WHEEL +1 -1
  43. ragaai_catalyst-2.1b0.dist-info/METADATA +0 -295
  44. ragaai_catalyst-2.1b0.dist-info/RECORD +0 -28
  45. {ragaai_catalyst-2.1b0.dist-info → ragaai_catalyst-2.1b1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,187 @@
1
+ import requests
2
+ import json
3
+ import os
4
+ from datetime import datetime
5
+
6
+
7
+ class UploadAgenticTraces:
8
+ def __init__(self,
9
+ json_file_path,
10
+ project_name,
11
+ project_id,
12
+ dataset_name,
13
+ user_detail,
14
+ base_url):
15
+ self.json_file_path = json_file_path
16
+ self.project_name = project_name
17
+ self.project_id = project_id
18
+ self.dataset_name = dataset_name
19
+ self.user_detail = user_detail
20
+ self.base_url = base_url
21
+ self.timeout = 99999
22
+
23
+ def _create_dataset_schema_with_trace(self):
24
+ SCHEMA_MAPPING_NEW = {
25
+ "trace_id": {"columnType": "traceId"},
26
+ "trace_uri": {"columnType": "traceUri"},
27
+ "prompt": {"columnType": "prompt"},
28
+ "response":{"columnType": "response"},
29
+ "context": {"columnType": "context"},
30
+ "llm_model": {"columnType":"pipeline"},
31
+ "recorded_on": {"columnType": "metadata"},
32
+ "embed_model": {"columnType":"pipeline"},
33
+ "log_source": {"columnType": "metadata"},
34
+ "vector_store":{"columnType":"pipeline"},
35
+ "feedback": {"columnType":"feedBack"}
36
+ }
37
+ def make_request():
38
+ headers = {
39
+ "Content-Type": "application/json",
40
+ "Authorization": f"Bearer {os.getenv('RAGAAI_CATALYST_TOKEN')}",
41
+ "X-Project-Name": self.project_name,
42
+ }
43
+ payload = json.dumps({
44
+ "datasetName": self.dataset_name,
45
+ # "schemaMapping": SCHEMA_MAPPING_NEW,
46
+ "traceFolderUrl": None,
47
+ })
48
+ response = requests.request("POST",
49
+ f"{self.base_url}/v1/llm/dataset/logs",
50
+ headers=headers,
51
+ data=payload,
52
+ timeout=self.timeout
53
+ )
54
+
55
+ return response
56
+
57
+ response = make_request()
58
+
59
+ if response.status_code == 401:
60
+ # get_token() # Fetch a new token and set it in the environment
61
+ response = make_request() # Retry the request
62
+ if response.status_code != 200:
63
+ return response.status_code
64
+ return response.status_code
65
+
66
+
67
+ def _get_presigned_url(self):
68
+ payload = json.dumps({
69
+ "datasetName": self.dataset_name,
70
+ "numFiles": 1,
71
+ })
72
+ headers = {
73
+ "Content-Type": "application/json",
74
+ "Authorization": f"Bearer {os.getenv('RAGAAI_CATALYST_TOKEN')}",
75
+ "X-Project-Name": self.project_name,
76
+ }
77
+
78
+ try:
79
+ response = requests.request("GET",
80
+ f"{self.base_url}/v1/llm/presigned-url",
81
+ headers=headers,
82
+ data=payload,
83
+ timeout=self.timeout)
84
+ if response.status_code == 200:
85
+ presignedUrls = response.json()["data"]["presignedUrls"][0]
86
+ return presignedUrls
87
+ except requests.exceptions.RequestException as e:
88
+ print(f"Error while getting presigned url: {e}")
89
+ return None
90
+
91
+ def _put_presigned_url(self, presignedUrl, filename):
92
+ headers = {
93
+ "Content-Type": "application/json",
94
+ }
95
+
96
+ if "blob.core.windows.net" in presignedUrl: # Azure
97
+ headers["x-ms-blob-type"] = "BlockBlob"
98
+ print(f"Uploading agentic traces...")
99
+ try:
100
+ with open(filename) as f:
101
+ payload = f.read().replace("\n", "").replace("\r", "").encode()
102
+ except Exception as e:
103
+ print(f"Error while reading file: {e}")
104
+ return None
105
+ try:
106
+ response = requests.request("PUT",
107
+ presignedUrl,
108
+ headers=headers,
109
+ data=payload,
110
+ timeout=self.timeout)
111
+ if response.status_code != 200 or response.status_code != 201:
112
+ return response, response.status_code
113
+ except requests.exceptions.RequestException as e:
114
+ print(f"Error while uploading to presigned url: {e}")
115
+ return None
116
+
117
+ def insert_traces(self, presignedUrl):
118
+ headers = {
119
+ "Authorization": f"Bearer {os.getenv('RAGAAI_CATALYST_TOKEN')}",
120
+ "Content-Type": "application/json",
121
+ "X-Project-Name": self.project_name,
122
+ }
123
+ payload = json.dumps({
124
+ "datasetName": self.dataset_name,
125
+ "presignedUrl": presignedUrl,
126
+ "datasetSpans": self._get_dataset_spans(), #Extra key for agentic traces
127
+ })
128
+ try:
129
+ response = requests.request("POST",
130
+ f"{self.base_url}/v1/llm/insert/trace",
131
+ headers=headers,
132
+ data=payload,
133
+ timeout=self.timeout)
134
+ except requests.exceptions.RequestException as e:
135
+ print(f"Error while inserting traces: {e}")
136
+ return None
137
+
138
+ def _get_dataset_spans(self):
139
+ try:
140
+ with open(self.json_file_path) as f:
141
+ data = json.load(f)
142
+ except Exception as e:
143
+ print(f"Error while reading file: {e}")
144
+ return None
145
+ try:
146
+ spans = data["data"][0]["spans"]
147
+ datasetSpans = []
148
+ for span in spans:
149
+ if span["type"] != "agent":
150
+ existing_span = next((s for s in datasetSpans if s["spanHash"] == span["hash_id"]), None)
151
+ if existing_span is None:
152
+ datasetSpans.append({
153
+ "spanId": span["id"],
154
+ "spanName": span["name"],
155
+ "spanHash": span["hash_id"],
156
+ "spanType": span["type"],
157
+ })
158
+ else:
159
+ datasetSpans.append({
160
+ "spanId": span["id"],
161
+ "spanName": span["name"],
162
+ "spanHash": span["hash_id"],
163
+ "spanType": span["type"],
164
+ })
165
+ children = span["data"]["children"]
166
+ for child in children:
167
+ existing_span = next((s for s in datasetSpans if s["spanHash"] == child["hash_id"]), None)
168
+ if existing_span is None:
169
+ datasetSpans.append({
170
+ "spanId": child["id"],
171
+ "spanName": child["name"],
172
+ "spanHash": child["hash_id"],
173
+ "spanType": child["type"],
174
+ })
175
+ return datasetSpans
176
+ except Exception as e:
177
+ print(f"Error while reading dataset spans: {e}")
178
+ return None
179
+
180
+ def upload_agentic_traces(self):
181
+ self._create_dataset_schema_with_trace()
182
+ presignedUrl = self._get_presigned_url()
183
+ if presignedUrl is None:
184
+ return
185
+ self._put_presigned_url(presignedUrl, self.json_file_path)
186
+ self.insert_traces(presignedUrl)
187
+ print("Agentic Traces uploaded")
@@ -0,0 +1,115 @@
1
+ from aiohttp import payload
2
+ import requests
3
+ import json
4
+ import os
5
+ import logging
6
+ logger = logging.getLogger(__name__)
7
+
8
+ def upload_code(hash_id, zip_path, project_name, dataset_name):
9
+ code_hashes_list = _fetch_dataset_code_hashes(project_name, dataset_name)
10
+
11
+ if hash_id not in code_hashes_list:
12
+ presigned_url = _fetch_presigned_url(project_name, dataset_name)
13
+ _put_zip_presigned_url(project_name, presigned_url, zip_path)
14
+
15
+ response = _insert_code(dataset_name, hash_id, presigned_url, project_name)
16
+ return response
17
+ else:
18
+ return "Code already exists"
19
+
20
+ def _fetch_dataset_code_hashes(project_name, dataset_name):
21
+ payload = {}
22
+ headers = {
23
+ "Authorization": f"Bearer {os.getenv('RAGAAI_CATALYST_TOKEN')}",
24
+ "X-Project-Name": project_name,
25
+ }
26
+
27
+ try:
28
+ response = requests.request("GET",
29
+ f"{os.getenv('RAGAAI_CATALYST_BASE_URL')}/v2/llm/dataset/code?datasetName={dataset_name}",
30
+ headers=headers,
31
+ data=payload,
32
+ timeout=99999)
33
+
34
+ if response.status_code == 200:
35
+ return response.json()["data"]["codeHashes"]
36
+ else:
37
+ raise Exception(f"Failed to fetch code hashes: {response.json()['message']}")
38
+ except requests.exceptions.RequestException as e:
39
+ logger.error(f"Failed to list datasets: {e}")
40
+ raise
41
+
42
+ def _fetch_presigned_url(project_name, dataset_name):
43
+ payload = json.dumps({
44
+ "datasetName": dataset_name,
45
+ "numFiles": 1,
46
+ "contentType": "application/zip"
47
+ })
48
+
49
+ headers = {
50
+ "Authorization": f"Bearer {os.getenv('RAGAAI_CATALYST_TOKEN')}",
51
+ "Content-Type": "application/json",
52
+ "X-Project-Name": project_name,
53
+ }
54
+
55
+ try:
56
+ response = requests.request("GET",
57
+ f"{os.getenv('RAGAAI_CATALYST_BASE_URL')}/v1/llm/presigned-url",
58
+ headers=headers,
59
+ data=payload,
60
+ timeout=99999)
61
+
62
+ if response.status_code == 200:
63
+ return response.json()["data"]["presignedUrls"][0]
64
+ else:
65
+ raise Exception(f"Failed to fetch code hashes: {response.json()['message']}")
66
+ except requests.exceptions.RequestException as e:
67
+ logger.error(f"Failed to list datasets: {e}")
68
+ raise
69
+
70
+ def _put_zip_presigned_url(project_name, presignedUrl, filename):
71
+ headers = {
72
+ "X-Project-Name": project_name,
73
+ "Content-Type": "application/zip",
74
+ }
75
+
76
+ if "blob.core.windows.net" in presignedUrl: # Azure
77
+ headers["x-ms-blob-type"] = "BlockBlob"
78
+ print(f"Uploading code...")
79
+ with open(filename, 'rb') as f:
80
+ payload = f.read()
81
+
82
+ response = requests.request("PUT",
83
+ presignedUrl,
84
+ headers=headers,
85
+ data=payload,
86
+ timeout=99999)
87
+ if response.status_code != 200 or response.status_code != 201:
88
+ return response, response.status_code
89
+
90
+ def _insert_code(dataset_name, hash_id, presigned_url, project_name):
91
+ payload = json.dumps({
92
+ "datasetName": dataset_name,
93
+ "codeHash": hash_id,
94
+ "presignedUrl": presigned_url
95
+ })
96
+
97
+ headers = {
98
+ 'X-Project-Name': project_name,
99
+ 'Content-Type': 'application/json',
100
+ 'Authorization': f'Bearer {os.getenv("RAGAAI_CATALYST_TOKEN")}'
101
+ }
102
+
103
+ try:
104
+ response = requests.request("POST",
105
+ f"{os.getenv('RAGAAI_CATALYST_BASE_URL')}/v2/llm/dataset/code",
106
+ headers=headers,
107
+ data=payload,
108
+ timeout=99999)
109
+ if response.status_code == 200:
110
+ return response.json()["message"]
111
+ else:
112
+ raise Exception(f"Failed to insert code: {response.json()['message']}")
113
+ except requests.exceptions.RequestException as e:
114
+ logger.error(f"Failed to insert code: {e}")
115
+ raise
@@ -0,0 +1,43 @@
1
+ import builtins
2
+ from datetime import datetime
3
+ import contextvars
4
+ import inspect
5
+ import uuid
6
+
7
+ class UserInteractionTracer:
8
+ def __init__(self, *args, **kwargs):
9
+ self.project_id = contextvars.ContextVar("project_id", default=None)
10
+ self.trace_id = contextvars.ContextVar("trace_id", default=None)
11
+ self.tracer = contextvars.ContextVar("tracer", default=None)
12
+ self.component_id = contextvars.ContextVar("component_id", default=None)
13
+ self.original_input = builtins.input
14
+ self.original_print = builtins.print
15
+ self.interactions = []
16
+
17
+ def traced_input(self, prompt=""):
18
+ # Get caller information
19
+ if prompt:
20
+ self.traced_print(prompt, end="")
21
+ try:
22
+ content = self.original_input()
23
+ except EOFError:
24
+ content = "" # Return empty string on EOF
25
+
26
+ self.interactions.append({
27
+ "id": str(uuid.uuid4()),
28
+ "interaction_type": "input",
29
+ "content": content,
30
+ "timestamp": datetime.now().isoformat()
31
+ })
32
+ return content
33
+
34
+ def traced_print(self, *args, **kwargs):
35
+ content = " ".join(str(arg) for arg in args)
36
+
37
+ self.interactions.append({
38
+ "id": str(uuid.uuid4()),
39
+ "interaction_type": "output",
40
+ "content": content,
41
+ "timestamp": datetime.now().isoformat()
42
+ })
43
+ return self.original_print(*args, **kwargs)
@@ -0,0 +1,3 @@
1
+ from .generic import get_db_path
2
+
3
+ __all__ = ["get_db_path"]
@@ -0,0 +1,18 @@
1
+ import requests
2
+
3
+ def fetch_analysis_trace(base_url, trace_id):
4
+ """
5
+ Fetches the analysis trace data from the server.
6
+
7
+ :param base_url: The base URL of the server (e.g., "http://localhost:3000").
8
+ :param trace_id: The ID of the trace to fetch.
9
+ :return: The JSON response from the server if successful, otherwise None.
10
+ """
11
+ try:
12
+ url = f"{base_url}/api/analysis_traces/{trace_id}"
13
+ response = requests.get(url)
14
+ response.raise_for_status() # Raise an error for bad responses (4xx, 5xx)
15
+ return response.json()
16
+ except requests.exceptions.RequestException as e:
17
+ print(f"Error fetching analysis trace: {e}")
18
+ return None
@@ -0,0 +1,61 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Dict, List, Any, Optional
3
+
4
+
5
+ @dataclass
6
+ class ProjectInfo:
7
+ project_name: str
8
+ start_time: float
9
+ end_time: float = field(default=0)
10
+ duration: float = field(default=0)
11
+ total_cost: float = field(default=0)
12
+ total_tokens: int = field(default=0)
13
+
14
+
15
+ @dataclass
16
+ class SystemInfo:
17
+ project_id: int
18
+ os_name: str
19
+ os_version: str
20
+ python_version: str
21
+ cpu_info: str
22
+ memory_total: float
23
+ installed_packages: str
24
+
25
+
26
+ @dataclass
27
+ class LLMCall:
28
+ name: str
29
+ model_name: str
30
+ input_prompt: str
31
+ output_response: str
32
+ tool_call: Dict
33
+ token_usage: Dict[str, int]
34
+ cost: Dict[str, float]
35
+ start_time: float = field(default=0)
36
+ end_time: float = field(default=0)
37
+ duration: float = field(default=0)
38
+
39
+
40
+ @dataclass
41
+ class ToolCall:
42
+ name: str
43
+ input_parameters: Dict[str, Any]
44
+ output: Any
45
+ start_time: float
46
+ end_time: float
47
+ duration: float
48
+ errors: Optional[str] = None
49
+
50
+
51
+ @dataclass
52
+ class AgentCall:
53
+ name: str
54
+ input_parameters: Dict[str, Any]
55
+ output: Any
56
+ start_time: float
57
+ end_time: float
58
+ duration: float
59
+ tool_calls: List[Dict[str, Any]]
60
+ llm_calls: List[Dict[str, Any]]
61
+ errors: Optional[str] = None
@@ -0,0 +1,32 @@
1
+ import os
2
+ import logging
3
+
4
+
5
+ def get_db_path():
6
+ db_filename = "trace_data.db"
7
+
8
+ # First, try the package directory
9
+ package_dir = os.path.dirname(os.path.abspath(__file__))
10
+ public_dir = os.path.join(package_dir, "..", "ui", "dist")
11
+ package_db_path = os.path.join(public_dir, db_filename)
12
+
13
+ # Ensure the directory exists
14
+ os.makedirs(os.path.dirname(package_db_path), exist_ok=True)
15
+
16
+ if os.path.exists(os.path.dirname(package_db_path)):
17
+ logging.debug(f"Using package database: {package_db_path}")
18
+ return f"sqlite:///{package_db_path}"
19
+
20
+ # Then, try the local directory
21
+ local_db_path = os.path.join(os.getcwd(), "agentneo", "ui", "dist", db_filename)
22
+ if os.path.exists(os.path.dirname(local_db_path)):
23
+ logging.debug(f"Using local database: {local_db_path}")
24
+ return f"sqlite:///{local_db_path}"
25
+
26
+ # Finally, try the local "/dist" directory
27
+ local_dist_path = os.path.join(os.getcwd(), "dist", db_filename)
28
+ if os.path.exists(os.path.dirname(local_dist_path)):
29
+ logging.debug(f"Using local database: {local_dist_path}")
30
+ return f"sqlite:///{local_dist_path}"
31
+
32
+ return f"sqlite:///{package_db_path}"
@@ -0,0 +1,177 @@
1
+ from .data_classes import LLMCall
2
+ from .trace_utils import (
3
+ calculate_cost,
4
+ convert_usage_to_dict,
5
+ load_model_costs,
6
+ )
7
+ from importlib import resources
8
+ import json
9
+ import os
10
+
11
+
12
+ # Load the Json configuration
13
+ try:
14
+ current_dir = os.path.dirname(os.path.abspath(__file__))
15
+ model_costs_path = os.path.join(current_dir, "model_costs.json")
16
+ with open(model_costs_path, "r") as file:
17
+ config = json.load(file)
18
+ except FileNotFoundError:
19
+ from importlib.resources import files
20
+ with (files("") / "model_costs.json").open("r") as file:
21
+ config = json.load(file)
22
+
23
+
24
+
25
+ def extract_llm_output(result):
26
+ # Initialize variables
27
+ model_name = None
28
+ output_response = ""
29
+ function_call = None
30
+ tool_call = None
31
+ token_usage = {}
32
+ cost = {}
33
+
34
+ # Try to get model_name from result or result.content
35
+ model_name = None
36
+ if hasattr(result, "model"):
37
+ model_name = result.model
38
+ elif hasattr(result, "content"):
39
+ try:
40
+ content_dict = json.loads(result.content)
41
+ model_name = content_dict.get("model", None)
42
+ except (json.JSONDecodeError, TypeError):
43
+ model_name = None
44
+
45
+ # Try to get choices from result or result.content
46
+ choices = None
47
+ if hasattr(result, "choices"):
48
+ choices = result.choices
49
+ elif hasattr(result, "content"):
50
+ try:
51
+ content_dict = json.loads(result.content)
52
+ choices = content_dict.get("choices", None)
53
+ except (json.JSONDecodeError, TypeError):
54
+ choices = None
55
+
56
+ if choices and len(choices) > 0:
57
+ first_choice = choices[0]
58
+
59
+ # Get message or text
60
+ message = None
61
+ if hasattr(first_choice, "message"):
62
+ message = first_choice.message
63
+ elif isinstance(first_choice, dict) and "message" in first_choice:
64
+ message = first_choice["message"]
65
+
66
+ if message:
67
+ # For chat completion
68
+ # Get output_response
69
+ if hasattr(message, "content"):
70
+ output_response = message.content
71
+ elif isinstance(message, dict) and "content" in message:
72
+ output_response = message["content"]
73
+
74
+ # Get function_call
75
+ if hasattr(message, "function_call"):
76
+ function_call = message.function_call
77
+ elif isinstance(message, dict) and "function_call" in message:
78
+ function_call = message["function_call"]
79
+
80
+ # Get tool_calls (if any)
81
+ if hasattr(message, "tool_calls"):
82
+ tool_call = message.tool_calls
83
+ elif isinstance(message, dict) and "tool_calls" in message:
84
+ tool_call = message["tool_calls"]
85
+ else:
86
+ # For completion
87
+ # Get output_response
88
+ if hasattr(first_choice, "text"):
89
+ output_response = first_choice.text
90
+ elif isinstance(first_choice, dict) and "text" in first_choice:
91
+ output_response = first_choice["text"]
92
+ else:
93
+ output_response = ""
94
+
95
+ # No message, so no function_call or tool_call
96
+ function_call = None
97
+ tool_call = None
98
+ else:
99
+ output_response = ""
100
+ function_call = None
101
+ tool_call = None
102
+
103
+ # Set tool_call to function_call if tool_call is None
104
+ if not tool_call:
105
+ tool_call = function_call
106
+
107
+ # Parse tool_call
108
+ parsed_tool_call = None
109
+ if tool_call:
110
+ if isinstance(tool_call, dict):
111
+ arguments = tool_call.get("arguments", "{}")
112
+ name = tool_call.get("name", "")
113
+ else:
114
+ # Maybe it's an object with attributes
115
+ arguments = getattr(tool_call, "arguments", "{}")
116
+ name = getattr(tool_call, "name", "")
117
+ try:
118
+ if isinstance(arguments, str):
119
+ arguments = json.loads(arguments)
120
+ else:
121
+ arguments = arguments # If already a dict
122
+ except json.JSONDecodeError:
123
+ arguments = {}
124
+ parsed_tool_call = {"arguments": arguments, "name": name}
125
+
126
+ # Try to get token_usage from result.usage or result.content
127
+ usage = None
128
+ if hasattr(result, "usage"):
129
+ usage = result.usage
130
+ elif hasattr(result, "content"):
131
+ try:
132
+ content_dict = json.loads(result.content)
133
+ usage = content_dict.get("usage", {})
134
+ except (json.JSONDecodeError, TypeError):
135
+ usage = {}
136
+ else:
137
+ usage = {}
138
+
139
+ token_usage = convert_usage_to_dict(usage)
140
+
141
+ # Load model costs
142
+ model_costs = load_model_costs()
143
+
144
+ # Calculate cost
145
+ if model_name in model_costs:
146
+ model_config = model_costs[model_name]
147
+ input_cost_per_token = model_config.get("input_cost_per_token", 0.0)
148
+ output_cost_per_token = model_config.get("output_cost_per_token", 0.0)
149
+ reasoning_cost_per_token = model_config.get(
150
+ "reasoning_cost_per_token", output_cost_per_token
151
+ )
152
+ else:
153
+ # Default costs or log a warning
154
+ print(
155
+ f"Warning: Model '{model_name}' not found in config. Using default costs."
156
+ )
157
+ input_cost_per_token = 0.0
158
+ output_cost_per_token = 0.0
159
+ reasoning_cost_per_token = 0.0
160
+
161
+ cost = calculate_cost(
162
+ token_usage,
163
+ input_cost_per_token=input_cost_per_token,
164
+ output_cost_per_token=output_cost_per_token,
165
+ reasoning_cost_per_token=reasoning_cost_per_token,
166
+ )
167
+
168
+ llm_data = LLMCall(
169
+ name="",
170
+ model_name=model_name,
171
+ input_prompt="", # Not available here
172
+ output_response=output_response,
173
+ token_usage=token_usage,
174
+ cost=cost,
175
+ tool_call=parsed_tool_call,
176
+ )
177
+ return llm_data