oracle-ads 2.12.9__py3-none-any.whl → 2.12.10rc0__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.
- ads/aqua/__init__.py +4 -4
- ads/aqua/common/enums.py +3 -0
- ads/aqua/common/utils.py +62 -2
- ads/aqua/data.py +2 -19
- ads/aqua/extension/finetune_handler.py +8 -14
- ads/aqua/extension/model_handler.py +19 -2
- ads/aqua/finetuning/constants.py +5 -2
- ads/aqua/finetuning/entities.py +64 -17
- ads/aqua/finetuning/finetuning.py +38 -54
- ads/aqua/model/entities.py +2 -1
- ads/aqua/model/model.py +61 -23
- ads/common/auth.py +9 -9
- ads/llm/autogen/__init__.py +2 -0
- ads/llm/autogen/constants.py +15 -0
- ads/llm/autogen/reports/__init__.py +2 -0
- ads/llm/autogen/reports/base.py +67 -0
- ads/llm/autogen/reports/data.py +103 -0
- ads/llm/autogen/reports/session.py +526 -0
- ads/llm/autogen/reports/templates/chat_box.html +13 -0
- ads/llm/autogen/reports/templates/chat_box_lt.html +5 -0
- ads/llm/autogen/reports/templates/chat_box_rt.html +6 -0
- ads/llm/autogen/reports/utils.py +56 -0
- ads/llm/autogen/v02/__init__.py +4 -0
- ads/llm/autogen/{client_v02.py → v02/client.py} +23 -10
- ads/llm/autogen/v02/log_handlers/__init__.py +2 -0
- ads/llm/autogen/v02/log_handlers/oci_file_handler.py +83 -0
- ads/llm/autogen/v02/loggers/__init__.py +6 -0
- ads/llm/autogen/v02/loggers/metric_logger.py +320 -0
- ads/llm/autogen/v02/loggers/session_logger.py +580 -0
- ads/llm/autogen/v02/loggers/utils.py +86 -0
- ads/llm/autogen/v02/runtime_logging.py +163 -0
- ads/llm/langchain/plugins/chat_models/oci_data_science.py +12 -11
- ads/model/__init__.py +11 -13
- ads/model/artifact.py +47 -8
- ads/model/extractor/embedding_onnx_extractor.py +80 -0
- ads/model/framework/embedding_onnx_model.py +438 -0
- ads/model/generic_model.py +26 -24
- ads/model/model_metadata.py +8 -7
- ads/opctl/config/merger.py +13 -14
- ads/opctl/operator/common/operator_config.py +4 -4
- ads/opctl/operator/lowcode/common/transformations.py +12 -5
- ads/opctl/operator/lowcode/common/utils.py +11 -5
- ads/opctl/operator/lowcode/forecast/const.py +2 -0
- ads/opctl/operator/lowcode/forecast/model/arima.py +19 -13
- ads/opctl/operator/lowcode/forecast/model/automlx.py +129 -36
- ads/opctl/operator/lowcode/forecast/model/autots.py +1 -0
- ads/opctl/operator/lowcode/forecast/model/base_model.py +61 -14
- ads/opctl/operator/lowcode/forecast/model/neuralprophet.py +10 -3
- ads/opctl/operator/lowcode/forecast/model/prophet.py +25 -18
- ads/opctl/operator/lowcode/forecast/schema.yaml +13 -0
- ads/opctl/operator/lowcode/forecast/utils.py +4 -3
- ads/telemetry/base.py +18 -11
- ads/telemetry/client.py +33 -13
- ads/templates/schemas/openapi.json +1740 -0
- ads/templates/score_embedding_onnx.jinja2 +202 -0
- {oracle_ads-2.12.9.dist-info → oracle_ads-2.12.10rc0.dist-info}/METADATA +7 -8
- {oracle_ads-2.12.9.dist-info → oracle_ads-2.12.10rc0.dist-info}/RECORD +60 -39
- {oracle_ads-2.12.9.dist-info → oracle_ads-2.12.10rc0.dist-info}/LICENSE.txt +0 -0
- {oracle_ads-2.12.9.dist-info → oracle_ads-2.12.10rc0.dist-info}/WHEEL +0 -0
- {oracle_ads-2.12.9.dist-info → oracle_ads-2.12.10rc0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,526 @@
|
|
1
|
+
# Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
|
2
|
+
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
|
3
|
+
"""Module for building session report."""
|
4
|
+
import copy
|
5
|
+
import json
|
6
|
+
import logging
|
7
|
+
from dataclasses import dataclass
|
8
|
+
from typing import List, Optional
|
9
|
+
|
10
|
+
import fsspec
|
11
|
+
import pandas as pd
|
12
|
+
import plotly.express as px
|
13
|
+
import report_creator as rc
|
14
|
+
|
15
|
+
from ads.common.auth import default_signer
|
16
|
+
from ads.llm.autogen.constants import Events
|
17
|
+
from ads.llm.autogen.reports.base import BaseReport
|
18
|
+
from ads.llm.autogen.reports.data import (
|
19
|
+
AgentData,
|
20
|
+
LLMCompletionData,
|
21
|
+
LogRecord,
|
22
|
+
ToolCallData,
|
23
|
+
)
|
24
|
+
from ads.llm.autogen.reports.utils import escape_html, get_duration, is_json_string
|
25
|
+
|
26
|
+
logger = logging.getLogger(__name__)
|
27
|
+
|
28
|
+
|
29
|
+
@dataclass
|
30
|
+
class AgentInvocation:
|
31
|
+
"""Represents an agent invocation."""
|
32
|
+
|
33
|
+
log: LogRecord
|
34
|
+
header: str = ""
|
35
|
+
description: str = ""
|
36
|
+
duration: Optional[float] = None
|
37
|
+
|
38
|
+
|
39
|
+
class SessionReport(BaseReport):
|
40
|
+
"""Class for building session report from session log file."""
|
41
|
+
|
42
|
+
def __init__(self, log_file: str, auth: Optional[dict] = None) -> None:
|
43
|
+
"""Initialize the session report with log file.
|
44
|
+
It is assumed that the file contains logs for a single session.
|
45
|
+
|
46
|
+
Parameters
|
47
|
+
----------
|
48
|
+
log_file : str
|
49
|
+
Path or URI of the log file.
|
50
|
+
auth : dict, optional
|
51
|
+
Authentication signer/config for OCI, by default None
|
52
|
+
"""
|
53
|
+
self.log_file: str = log_file
|
54
|
+
if self.log_file.startswith("oci://"):
|
55
|
+
auth = auth or default_signer()
|
56
|
+
with fsspec.open(self.log_file, mode="r", **auth) as f:
|
57
|
+
self.log_lines = f.readlines()
|
58
|
+
else:
|
59
|
+
with open(self.log_file, encoding="utf-8") as f:
|
60
|
+
self.log_lines = f.readlines()
|
61
|
+
self.logs: List[LogRecord] = self._parse_logs()
|
62
|
+
|
63
|
+
# Parse logs to get entities for building the report
|
64
|
+
# Agents
|
65
|
+
self.agents: List[AgentData] = self._parse_agents()
|
66
|
+
self.managers: List[AgentData] = self._parse_managers()
|
67
|
+
# Events
|
68
|
+
self.start_event: LogRecord = self._parse_start_event()
|
69
|
+
self.session_id: str = self.start_event.session_id
|
70
|
+
self.llm_calls: List[AgentInvocation] = self._parse_llm_calls()
|
71
|
+
self.tool_calls: List[AgentInvocation] = self._parse_tool_calls()
|
72
|
+
self.invocations: List[AgentInvocation] = self._parse_invocations()
|
73
|
+
|
74
|
+
self.received_message_logs = self._parse_received_messages()
|
75
|
+
|
76
|
+
def _parse_logs(self) -> List[LogRecord]:
|
77
|
+
"""Parses the logs form strings into LogRecord objects."""
|
78
|
+
logs = []
|
79
|
+
for i, log in enumerate(self.log_lines):
|
80
|
+
try:
|
81
|
+
logs.append(LogRecord.from_dict(json.loads(log)))
|
82
|
+
except Exception as e:
|
83
|
+
logger.error(
|
84
|
+
"Error when parsing log record at line %s:\n%s", str(i + 1), str(e)
|
85
|
+
)
|
86
|
+
continue
|
87
|
+
# Sort the logs by timestamp
|
88
|
+
logs = sorted(logs, key=lambda x: x.timestamp)
|
89
|
+
return logs
|
90
|
+
|
91
|
+
def _parse_agents(self) -> List[AgentData]:
|
92
|
+
"""Parses the logs to identify unique agents.
|
93
|
+
AutoGen may have new_agent multiple times.
|
94
|
+
Here we identify the agents by the unique tuple of (name, module, class).
|
95
|
+
"""
|
96
|
+
new_agent_logs = self.filter_by_event(Events.NEW_AGENT)
|
97
|
+
agents = {}
|
98
|
+
for log in new_agent_logs:
|
99
|
+
agent: AgentData = log.data
|
100
|
+
agents[(agent.agent_name, agent.agent_module, agent.agent_class)] = agent
|
101
|
+
return list(agents.values())
|
102
|
+
|
103
|
+
def _parse_managers(self) -> List[AgentData]:
|
104
|
+
"""Parses the logs to get chat managers."""
|
105
|
+
managers = []
|
106
|
+
for agent in self.agents:
|
107
|
+
if agent.is_manager:
|
108
|
+
managers.append(agent)
|
109
|
+
return managers
|
110
|
+
|
111
|
+
def _parse_start_event(self) -> LogRecord:
|
112
|
+
"""Parses the logs to get the first logging_session_start event log."""
|
113
|
+
records = self.filter_by_event(event_name=Events.SESSION_START)
|
114
|
+
if not records:
|
115
|
+
raise ValueError("logging_session_start event is not found in the logs.")
|
116
|
+
records = sorted(records, key=lambda x: x.timestamp)
|
117
|
+
return records[0]
|
118
|
+
|
119
|
+
def _parse_llm_calls(self) -> List[AgentInvocation]:
|
120
|
+
"""Parses the logs to get the LLM calls."""
|
121
|
+
records = self.filter_by_event(Events.LLM_CALL)
|
122
|
+
invocations = []
|
123
|
+
for record in records:
|
124
|
+
log_data: LLMCompletionData = record.data
|
125
|
+
source_name = record.source_name
|
126
|
+
request = log_data.request
|
127
|
+
# If there is no request, the log is invalid.
|
128
|
+
if not request:
|
129
|
+
continue
|
130
|
+
|
131
|
+
header = f"{source_name} invoking {request.get('model')}"
|
132
|
+
if log_data.is_cached:
|
133
|
+
header += " (Cached)"
|
134
|
+
invocations.append(
|
135
|
+
AgentInvocation(
|
136
|
+
header=header,
|
137
|
+
log=record,
|
138
|
+
duration=get_duration(log_data.start_time, log_data.end_time),
|
139
|
+
)
|
140
|
+
)
|
141
|
+
return invocations
|
142
|
+
|
143
|
+
def _parse_tool_calls(self) -> List[AgentInvocation]:
|
144
|
+
"""Parses the logs to get the tool calls."""
|
145
|
+
records = self.filter_by_event(Events.TOOL_CALL)
|
146
|
+
invocations = []
|
147
|
+
for record in records:
|
148
|
+
log_data: ToolCallData = record.data
|
149
|
+
source_name = record.source_name
|
150
|
+
invocations.append(
|
151
|
+
AgentInvocation(
|
152
|
+
log=record,
|
153
|
+
header=f"{source_name} invoking {log_data.tool_name}",
|
154
|
+
duration=get_duration(log_data.start_time, log_data.end_time),
|
155
|
+
)
|
156
|
+
)
|
157
|
+
return invocations
|
158
|
+
|
159
|
+
def _parse_invocations(self) -> List[AgentInvocation]:
|
160
|
+
"""Add numbering to the combined list of LLM and tool calls."""
|
161
|
+
invocations = self.llm_calls + self.tool_calls
|
162
|
+
invocations = sorted(invocations, key=lambda x: x.log.data.start_time)
|
163
|
+
for i, invocation in enumerate(invocations):
|
164
|
+
invocation.header = f"{str(i + 1)} {invocation.header}"
|
165
|
+
return invocations
|
166
|
+
|
167
|
+
def _parse_received_messages(self) -> List[LogRecord]:
|
168
|
+
"""Parses the logs to get the received_message events."""
|
169
|
+
managers = [manager.agent_name for manager in self.managers]
|
170
|
+
logs = self.filter_by_event(Events.RECEIVED_MESSAGE)
|
171
|
+
if not logs:
|
172
|
+
return []
|
173
|
+
logs = sorted(logs, key=lambda x: x.timestamp)
|
174
|
+
logs = [log for log in logs if log.kwargs.get("sender") not in managers]
|
175
|
+
return logs
|
176
|
+
|
177
|
+
def filter_by_event(self, event_name: str) -> List[LogRecord]:
|
178
|
+
"""Filters the logs by event name.
|
179
|
+
|
180
|
+
Parameters
|
181
|
+
----------
|
182
|
+
event_name : str
|
183
|
+
Name of the event.
|
184
|
+
|
185
|
+
Returns
|
186
|
+
-------
|
187
|
+
List[LogRecord]
|
188
|
+
A list of LogRecord objects for the event.
|
189
|
+
"""
|
190
|
+
filtered_logs = []
|
191
|
+
for log in self.logs:
|
192
|
+
if log.event_name == event_name:
|
193
|
+
filtered_logs.append(log)
|
194
|
+
return filtered_logs
|
195
|
+
|
196
|
+
def _build_flowchart(self):
|
197
|
+
"""Builds the flowchart of agent chats."""
|
198
|
+
senders = []
|
199
|
+
for log in self.received_message_logs:
|
200
|
+
sender = log.kwargs.get("sender")
|
201
|
+
senders.append(sender)
|
202
|
+
|
203
|
+
diagram_src = "graph LR\n"
|
204
|
+
prev_sender = None
|
205
|
+
links = []
|
206
|
+
# Conversation Flow
|
207
|
+
for sender in senders:
|
208
|
+
if prev_sender is None:
|
209
|
+
link = f"START([START]) --> {sender}"
|
210
|
+
else:
|
211
|
+
link = f"{prev_sender} --> {sender}"
|
212
|
+
if link not in links:
|
213
|
+
links.append(link)
|
214
|
+
prev_sender = sender
|
215
|
+
links.append(f"{prev_sender} --> END([END])")
|
216
|
+
# Tool Calls
|
217
|
+
for invocation in self.tool_calls:
|
218
|
+
tool = invocation.log.data.tool_name
|
219
|
+
agent = invocation.log.data.agent_name
|
220
|
+
if tool and agent:
|
221
|
+
link = f"{agent} <--> {tool}[[{tool}]]"
|
222
|
+
if link not in links:
|
223
|
+
links.append(link)
|
224
|
+
|
225
|
+
diagram_src += "\n".join(links)
|
226
|
+
return rc.Diagram(src=diagram_src, label="Flowchart")
|
227
|
+
|
228
|
+
def _build_timeline_tab(self):
|
229
|
+
"""Builds the plotly timeline chart."""
|
230
|
+
if not self.invocations:
|
231
|
+
return rc.Text("No LLM or Tool Calls.", label="Timeline")
|
232
|
+
invocations = []
|
233
|
+
for invocation in self.invocations:
|
234
|
+
invocations.append(
|
235
|
+
{
|
236
|
+
"start_time": invocation.log.data.start_time,
|
237
|
+
"end_time": invocation.log.data.end_time,
|
238
|
+
"header": invocation.header,
|
239
|
+
"duration": invocation.duration,
|
240
|
+
}
|
241
|
+
)
|
242
|
+
df = pd.DataFrame(invocations)
|
243
|
+
fig = px.timeline(
|
244
|
+
df,
|
245
|
+
x_start="start_time",
|
246
|
+
x_end="end_time",
|
247
|
+
y="header",
|
248
|
+
labels={"header": "Invocation"},
|
249
|
+
color="duration",
|
250
|
+
color_continuous_scale="rdylgn_r",
|
251
|
+
height=max(len(df.index) * 50, 500),
|
252
|
+
)
|
253
|
+
fig.update_layout(showlegend=False)
|
254
|
+
fig.update_yaxes(autorange="reversed")
|
255
|
+
return rc.Block(
|
256
|
+
rc.Widget(fig, label="Timeline"), self._build_flowchart(), label="Timeline"
|
257
|
+
)
|
258
|
+
|
259
|
+
def _format_messages(self, messages: List[dict]):
|
260
|
+
"""Formats the LLM call messages to be displayed in the report."""
|
261
|
+
text = ""
|
262
|
+
for message in messages:
|
263
|
+
text += f"**{message.get('role')}**:\n{message.get('content')}\n\n"
|
264
|
+
return text
|
265
|
+
|
266
|
+
def _build_llm_call(self, invocation: AgentInvocation):
|
267
|
+
"""Builds the LLM call details."""
|
268
|
+
log_data: LLMCompletionData = invocation.log.data
|
269
|
+
request = log_data.request
|
270
|
+
response = log_data.response
|
271
|
+
|
272
|
+
start_date, start_time = self._parse_date_time(log_data.start_time)
|
273
|
+
|
274
|
+
request_value = f"{str(len(request.get('messages')))} messages"
|
275
|
+
tools = request.get("tools", [])
|
276
|
+
if tools:
|
277
|
+
request_value += f", {str(len(tools))} tools"
|
278
|
+
|
279
|
+
response_message = response.get("choices")[0].get("message")
|
280
|
+
response_text = response_message.get("content") or ""
|
281
|
+
tool_calls = response_message.get("tool_calls")
|
282
|
+
if tool_calls:
|
283
|
+
response_text += "\n\n**Tool Calls**:"
|
284
|
+
for tool_call in tool_calls:
|
285
|
+
func = tool_call.get("function")
|
286
|
+
response_text += f"\n\n`{func.get('name')}(**{func.get('arguments')})`"
|
287
|
+
|
288
|
+
metrics = [
|
289
|
+
rc.Metric(heading="Time", value=start_time, label=start_date),
|
290
|
+
rc.Metric(
|
291
|
+
heading="Messages",
|
292
|
+
value=len(request.get("messages", [])),
|
293
|
+
),
|
294
|
+
rc.Metric(heading="Tools", value=len(tools)),
|
295
|
+
rc.Metric(heading="Duration", value=invocation.duration, unit="s"),
|
296
|
+
rc.Metric(
|
297
|
+
heading="Cached",
|
298
|
+
value="Yes" if log_data.is_cached else "No",
|
299
|
+
),
|
300
|
+
rc.Metric(heading="Cost", value=log_data.cost),
|
301
|
+
]
|
302
|
+
|
303
|
+
usage = response.get("usage")
|
304
|
+
if isinstance(usage, dict):
|
305
|
+
for k, v in usage.items():
|
306
|
+
if not v:
|
307
|
+
continue
|
308
|
+
metrics.append(
|
309
|
+
rc.Metric(heading=str(k).replace("_", " ").title(), value=v)
|
310
|
+
)
|
311
|
+
|
312
|
+
return rc.Block(
|
313
|
+
rc.Block(rc.Group(*metrics, label=invocation.header)),
|
314
|
+
rc.Group(
|
315
|
+
rc.Block(
|
316
|
+
rc.Markdown(
|
317
|
+
self._format_messages(request.get("messages")), label="Request"
|
318
|
+
),
|
319
|
+
rc.Collapse(
|
320
|
+
rc.Json(request),
|
321
|
+
label="JSON",
|
322
|
+
),
|
323
|
+
),
|
324
|
+
rc.Block(
|
325
|
+
rc.Markdown(response_text, label="Response"),
|
326
|
+
rc.Collapse(
|
327
|
+
rc.Json(response),
|
328
|
+
label="JSON",
|
329
|
+
),
|
330
|
+
),
|
331
|
+
),
|
332
|
+
)
|
333
|
+
|
334
|
+
def _build_tool_call(self, invocation: AgentInvocation):
|
335
|
+
"""Builds the tool call details."""
|
336
|
+
log_data: ToolCallData = invocation.log.data
|
337
|
+
request = log_data.to_dict()
|
338
|
+
response = request.pop("returns", {})
|
339
|
+
|
340
|
+
start_date, start_time = self._parse_date_time(log_data.start_time)
|
341
|
+
tool_call_args = log_data.input_args
|
342
|
+
if is_json_string(tool_call_args):
|
343
|
+
tool_call_args = self.format_json_string(tool_call_args)
|
344
|
+
|
345
|
+
if is_json_string(response):
|
346
|
+
response = self.format_json_string(response)
|
347
|
+
|
348
|
+
metrics = [
|
349
|
+
rc.Metric(heading="Time", value=start_time, label=start_date),
|
350
|
+
rc.Metric(heading="Duration", value=invocation.duration, unit="s"),
|
351
|
+
]
|
352
|
+
|
353
|
+
return rc.Block(
|
354
|
+
rc.Block(rc.Group(*metrics, label=invocation.header)),
|
355
|
+
rc.Group(
|
356
|
+
rc.Block(
|
357
|
+
rc.Markdown(
|
358
|
+
(log_data.tool_name or "") + "\n\n" + tool_call_args,
|
359
|
+
label="Request",
|
360
|
+
),
|
361
|
+
rc.Collapse(
|
362
|
+
rc.Json(request),
|
363
|
+
label="JSON",
|
364
|
+
),
|
365
|
+
),
|
366
|
+
rc.Block(rc.Text("", label="Response"), rc.Markdown(response)),
|
367
|
+
),
|
368
|
+
)
|
369
|
+
|
370
|
+
def _build_invocations_tab(self) -> rc.Block:
|
371
|
+
"""Builds the invocations tab."""
|
372
|
+
blocks = []
|
373
|
+
for invocation in self.invocations:
|
374
|
+
event_name = invocation.log.event_name
|
375
|
+
if event_name == Events.LLM_CALL:
|
376
|
+
blocks.append(self._build_llm_call(invocation))
|
377
|
+
elif event_name == Events.TOOL_CALL:
|
378
|
+
blocks.append(self._build_tool_call(invocation))
|
379
|
+
return rc.Block(
|
380
|
+
*blocks,
|
381
|
+
label="Invocations",
|
382
|
+
)
|
383
|
+
|
384
|
+
def _build_chat_tab(self) -> rc.Block:
|
385
|
+
"""Builds the chat tab."""
|
386
|
+
if not self.received_message_logs:
|
387
|
+
return rc.Text("No messages received in this session.", label="Chats")
|
388
|
+
# The agent sending the first message will be placed on the right.
|
389
|
+
# All other agents will be placed on the left
|
390
|
+
host = self.received_message_logs[0].kwargs.get("sender")
|
391
|
+
blocks = []
|
392
|
+
|
393
|
+
for log in self.received_message_logs:
|
394
|
+
context = copy.deepcopy(log.kwargs)
|
395
|
+
context.update(log.to_dict())
|
396
|
+
sender = context.get("sender")
|
397
|
+
message = context.get("message", "")
|
398
|
+
# Content
|
399
|
+
if isinstance(message, dict) and "content" in message:
|
400
|
+
content = message.get("content", "")
|
401
|
+
if is_json_string(content):
|
402
|
+
context["json_content"] = json.dumps(json.loads(content), indent=2)
|
403
|
+
context["content"] = content
|
404
|
+
else:
|
405
|
+
context["content"] = message
|
406
|
+
if context["content"] is None:
|
407
|
+
context["content"] = ""
|
408
|
+
# Tool call
|
409
|
+
if isinstance(message, dict) and "tool_calls" in message:
|
410
|
+
tool_calls = message.get("tool_calls")
|
411
|
+
if tool_calls:
|
412
|
+
tool_call_signatures = []
|
413
|
+
for tool_call in tool_calls:
|
414
|
+
func = tool_call.get("function")
|
415
|
+
if not func:
|
416
|
+
continue
|
417
|
+
tool_call_signatures.append(
|
418
|
+
f'{func.get("name")}(**{func.get("arguments", "{}")})'
|
419
|
+
)
|
420
|
+
context["tool_calls"] = tool_call_signatures
|
421
|
+
if sender == host:
|
422
|
+
html = self._render_template("chat_box_rt.html", **context)
|
423
|
+
else:
|
424
|
+
html = self._render_template("chat_box_lt.html", **context)
|
425
|
+
blocks.append(rc.Html(html))
|
426
|
+
|
427
|
+
return rc.Block(
|
428
|
+
*blocks,
|
429
|
+
label="Chats",
|
430
|
+
)
|
431
|
+
|
432
|
+
def _build_logs_tab(self) -> rc.Block:
|
433
|
+
"""Builds the logs tab."""
|
434
|
+
blocks = []
|
435
|
+
for log_line in self.log_lines:
|
436
|
+
if is_json_string(log_line):
|
437
|
+
log = json.loads(log_line)
|
438
|
+
label = log.get(
|
439
|
+
"event_name", self._preview_message(log.get("message", ""))
|
440
|
+
)
|
441
|
+
blocks.append(rc.Collapse(rc.Json(escape_html(log)), label=label))
|
442
|
+
else:
|
443
|
+
log = log_line
|
444
|
+
blocks.append(
|
445
|
+
rc.Collapse(rc.Text(log), label=self._preview_message(log_line))
|
446
|
+
)
|
447
|
+
|
448
|
+
return rc.Block(
|
449
|
+
*blocks,
|
450
|
+
label="Logs",
|
451
|
+
)
|
452
|
+
|
453
|
+
def _build_errors_tab(self) -> Optional[rc.Block]:
|
454
|
+
"""Builds the error tab to show exception."""
|
455
|
+
errors = self.filter_by_event(Events.EXCEPTION)
|
456
|
+
if not errors:
|
457
|
+
return None
|
458
|
+
blocks = []
|
459
|
+
for error in errors:
|
460
|
+
label = f'{error.kwargs.get("exc_type", "")} - {error.kwargs.get("exc_value", "")}'
|
461
|
+
variables: dict = error.kwargs.get("locals", {})
|
462
|
+
table = "| Variable | Value |\n|---|---|\n"
|
463
|
+
table += "\n".join([f"| {k} | {v} |" for k, v in variables.items()])
|
464
|
+
blocks += [
|
465
|
+
rc.Unformatted(text=error.kwargs.get("traceback", ""), label=label),
|
466
|
+
rc.Markdown(table),
|
467
|
+
]
|
468
|
+
return rc.Block(*blocks, label="Error")
|
469
|
+
|
470
|
+
def build(self, output_file: str):
|
471
|
+
"""Builds the session report.
|
472
|
+
|
473
|
+
Parameters
|
474
|
+
----------
|
475
|
+
output_file : str
|
476
|
+
Local path or OCI object storage URI to save the report HTML file.
|
477
|
+
"""
|
478
|
+
|
479
|
+
if not self.managers:
|
480
|
+
agent_label = ""
|
481
|
+
elif len(self.managers) == 1:
|
482
|
+
agent_label = "+1 chat manager"
|
483
|
+
else:
|
484
|
+
agent_label = f"+{str(len(self.managers))} chat managers"
|
485
|
+
|
486
|
+
blocks = [
|
487
|
+
self._build_timeline_tab(),
|
488
|
+
self._build_invocations_tab(),
|
489
|
+
self._build_chat_tab(),
|
490
|
+
self._build_logs_tab(),
|
491
|
+
]
|
492
|
+
|
493
|
+
error_block = self._build_errors_tab()
|
494
|
+
if error_block:
|
495
|
+
blocks.append(error_block)
|
496
|
+
|
497
|
+
with rc.ReportCreator(
|
498
|
+
title=f"AutoGen Session: {self.session_id}",
|
499
|
+
description=f"Started at {self.start_event.timestamp}",
|
500
|
+
footer="Created with ❤️ by Oracle ADS",
|
501
|
+
) as report:
|
502
|
+
|
503
|
+
view = rc.Block(
|
504
|
+
rc.Group(
|
505
|
+
rc.Metric(
|
506
|
+
heading="Agents",
|
507
|
+
value=len(self.agents) - len(self.managers),
|
508
|
+
label=agent_label,
|
509
|
+
),
|
510
|
+
rc.Metric(
|
511
|
+
heading="Events",
|
512
|
+
value=len(self.logs),
|
513
|
+
),
|
514
|
+
rc.Metric(
|
515
|
+
heading="LLM Calls",
|
516
|
+
value=len(self.llm_calls),
|
517
|
+
),
|
518
|
+
rc.Metric(
|
519
|
+
heading="Tool Calls",
|
520
|
+
value=len(self.tool_calls),
|
521
|
+
),
|
522
|
+
),
|
523
|
+
rc.Select(blocks=blocks),
|
524
|
+
)
|
525
|
+
|
526
|
+
report.save(view, output_file)
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<p><strong>{{ sender }}</strong><br /><small><i>to {{ source_name }}</i></small></p>
|
2
|
+
<p><small><i>{{ timestamp }}</i></small></p>
|
3
|
+
<hr />
|
4
|
+
{% if json_content %}
|
5
|
+
<pre><code style="background-color: white; text-align: left;">{{ json_content }}</code></pre>
|
6
|
+
{% else%}
|
7
|
+
<p>{{ content }}</p>
|
8
|
+
{% endif %}
|
9
|
+
{% if tool_calls %}
|
10
|
+
{% for tool_call in tool_calls %}
|
11
|
+
<pre><code style="background-color: white; text-align: left;">{{ tool_call }}</code></pre>
|
12
|
+
{% endfor %}
|
13
|
+
{% endif %}
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
|
2
|
+
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
|
3
|
+
import html
|
4
|
+
import json
|
5
|
+
from datetime import datetime
|
6
|
+
|
7
|
+
|
8
|
+
def parse_datetime(s):
|
9
|
+
return datetime.strptime(s, "%Y-%m-%d %H:%M:%S.%f")
|
10
|
+
|
11
|
+
|
12
|
+
def get_duration(start_time: str, end_time: str) -> float:
|
13
|
+
"""Gets the duration in seconds between `start_time` and `end_time`.
|
14
|
+
Each of the value should be a time in string format of
|
15
|
+
`%Y-%m-%d %H:%M:%S.%f`
|
16
|
+
|
17
|
+
The duration is calculated by parsing the two strings,
|
18
|
+
then subtracting the `end_time` from `start_time`.
|
19
|
+
|
20
|
+
If either `start_time` or `end_time` is not presented,
|
21
|
+
0 will be returned.
|
22
|
+
|
23
|
+
Parameters
|
24
|
+
----------
|
25
|
+
start_time : str
|
26
|
+
The start time.
|
27
|
+
end_time : str
|
28
|
+
The end time.
|
29
|
+
|
30
|
+
Returns
|
31
|
+
-------
|
32
|
+
float
|
33
|
+
Duration in seconds.
|
34
|
+
"""
|
35
|
+
if not start_time or not end_time:
|
36
|
+
return 0
|
37
|
+
return (parse_datetime(end_time) - parse_datetime(start_time)).total_seconds()
|
38
|
+
|
39
|
+
|
40
|
+
def is_json_string(s):
|
41
|
+
"""Checks if a string contains valid JSON."""
|
42
|
+
try:
|
43
|
+
json.loads(s)
|
44
|
+
except Exception:
|
45
|
+
return False
|
46
|
+
return True
|
47
|
+
|
48
|
+
|
49
|
+
def escape_html(obj):
|
50
|
+
if isinstance(obj, dict):
|
51
|
+
return {k: escape_html(v) for k, v in obj.items()}
|
52
|
+
elif isinstance(obj, list):
|
53
|
+
return [escape_html(v) for v in obj]
|
54
|
+
elif isinstance(obj, str):
|
55
|
+
return html.escape(obj)
|
56
|
+
return html.escape(str(obj))
|