youtu-agent 0.1.2__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.
utils/__init__.py ADDED
@@ -0,0 +1,35 @@
1
+ from .agents_utils import AgentsUtils, ChatCompletionConverter
2
+ from .common import get_event_loop, load_class_from_file, schema_to_basemodel
3
+ from .env import EnvUtils
4
+ from .image import encode_image
5
+ from .llm_output_parser import LLMOutputParser
6
+ from .log import get_logger, oneline_object, setup_logging
7
+ from .openai_utils import OpenAIUtils, SimplifiedAsyncOpenAI
8
+ from .path import CACHE_DIR, DIR_ROOT, FileUtils
9
+ from .print_utils import PrintUtils
10
+ from .sqlmodel_utils import SQLModelUtils
11
+ from .token import TokenUtils
12
+ from .tool_cache import async_file_cache
13
+
14
+ __all__ = [
15
+ "PrintUtils",
16
+ "SimplifiedAsyncOpenAI",
17
+ "OpenAIUtils",
18
+ "AgentsUtils",
19
+ "ChatCompletionConverter",
20
+ "oneline_object",
21
+ "setup_logging",
22
+ "schema_to_basemodel",
23
+ "load_class_from_file",
24
+ "get_logger",
25
+ "SQLModelUtils",
26
+ "async_file_cache",
27
+ "DIR_ROOT",
28
+ "FileUtils",
29
+ "CACHE_DIR",
30
+ "TokenUtils",
31
+ "get_event_loop",
32
+ "EnvUtils",
33
+ "LLMOutputParser",
34
+ "encode_image",
35
+ ]
utils/agents_utils.py ADDED
@@ -0,0 +1,359 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ import uuid
5
+ from collections.abc import AsyncIterator, Iterable
6
+ from typing import Literal
7
+
8
+ from agents import (
9
+ FunctionTool,
10
+ HandoffOutputItem,
11
+ ItemHelpers,
12
+ MessageOutputItem,
13
+ Model,
14
+ ModelSettings,
15
+ ModelTracing,
16
+ OpenAIChatCompletionsModel,
17
+ OpenAIResponsesModel,
18
+ ReasoningItem,
19
+ RunItem,
20
+ RunResult,
21
+ StreamEvent,
22
+ ToolCallItem,
23
+ ToolCallOutputItem,
24
+ TResponseInputItem,
25
+ )
26
+ from agents.models.chatcmpl_converter import Converter
27
+ from agents.stream_events import AgentUpdatedStreamEvent, RawResponsesStreamEvent, RunItemStreamEvent
28
+ from agents.tracing import Trace, gen_trace_id, get_current_trace
29
+ from openai import AsyncOpenAI
30
+ from openai.types.chat import ChatCompletionMessageParam, ChatCompletionToolParam
31
+ from openai.types.responses import ResponseFunctionToolCall
32
+
33
+ from .image import encode_image
34
+ from .openai_utils import OpenAIChatCompletionParams
35
+ from .print_utils import PrintUtils
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ class ChatCompletionConverter(Converter):
41
+ @classmethod
42
+ def items_to_messages(cls, items: str | Iterable[TResponseInputItem]) -> list[ChatCompletionMessageParam]:
43
+ # skip reasoning, see chatcmpl_converter.Converter.items_to_messages()
44
+ # agents.exceptions.UserError: Unhandled item type or structure:
45
+ # {'id': '__fake_id__', 'summary': [{'text': '...', 'type': 'summary_text'}], 'type': 'reasoning'}
46
+ if not isinstance(items, str): # TODO: check it!
47
+ items = cls.filter_items(items)
48
+ return Converter.items_to_messages(items)
49
+
50
+ @classmethod
51
+ def filter_items(cls, items: str | Iterable[TResponseInputItem]) -> str | list[TResponseInputItem]:
52
+ if isinstance(items, str):
53
+ return items
54
+ filtered_items = []
55
+ for item in items:
56
+ if item.get("type", None) == "reasoning":
57
+ continue
58
+ filtered_items.append(item)
59
+ return filtered_items
60
+
61
+ @classmethod
62
+ def items_to_dict(cls, items: str | Iterable[TResponseInputItem]) -> list[dict]:
63
+ """convert items to a list of dict which have {"role", "content"}
64
+ WIP!
65
+ """
66
+ if isinstance(items, str):
67
+ return [{"role": "user", "content": items}]
68
+ result = []
69
+ for item in items:
70
+ if msg := Converter.maybe_easy_input_message(item):
71
+ result.append(msg)
72
+ elif msg := Converter.maybe_input_message(item):
73
+ result.append(msg)
74
+ elif msg := Converter.maybe_response_output_message(item):
75
+ result.append(msg)
76
+ elif msg := Converter.maybe_file_search_call(item):
77
+ msg.update({"role": "tool", "content": msg["results"]})
78
+ result.append(msg)
79
+ elif msg := Converter.maybe_function_tool_call(item):
80
+ msg.update({"role": "assistant", "content": f"{msg['name']}({msg['arguments']})"})
81
+ result.append(msg)
82
+ elif msg := Converter.maybe_function_tool_call_output(item):
83
+ msg.update({"role": "tool", "content": msg["output"], "tool_call_id": msg["call_id"]})
84
+ result.append(msg)
85
+ elif msg := Converter.maybe_reasoning_message(item):
86
+ msg.update({"role": "assistant", "content": msg["summary"]})
87
+ result.append(msg)
88
+ else:
89
+ logger.warning(f"Unknown message type: {item}")
90
+ result.append({"role": "assistant", "content": f"Unknown message type: {item}"})
91
+ return result
92
+
93
+
94
+ class AgentsUtils:
95
+ """Utils for openai-agents SDK"""
96
+
97
+ @staticmethod
98
+ def generate_group_id() -> str:
99
+ """Generate a unique group ID. (Used in OpenAI tracing)
100
+ Ref: https://openai.github.io/openai-agents-python/tracing/
101
+ """
102
+ return uuid.uuid4().hex[:16]
103
+
104
+ @staticmethod
105
+ def gen_trace_id() -> str:
106
+ return gen_trace_id()
107
+
108
+ @staticmethod
109
+ def get_current_trace() -> Trace:
110
+ return get_current_trace()
111
+
112
+ @staticmethod
113
+ def get_agents_model(
114
+ type: Literal["responses", "chat.completions", "litellm"] = None,
115
+ model: str = None,
116
+ base_url: str = None,
117
+ api_key: str = None,
118
+ ) -> Model:
119
+ type = type or os.getenv("UTU_LLM_TYPE", "chat.completions")
120
+ model = model or os.getenv("UTU_LLM_MODEL")
121
+ if type == "litellm":
122
+ # Ref: https://docs.litellm.ai/docs/providers
123
+ # NOTE: should set .evn properly! e.g. AZURE_API_KEY, AZURE_API_BASE, AZURE_API_VERSION for Azure
124
+ # https://docs.litellm.ai/docs/providers/azure/
125
+ from agents.extensions.models.litellm_model import LitellmModel
126
+
127
+ return LitellmModel(model=model)
128
+
129
+ base_url = base_url or os.getenv("UTU_LLM_BASE_URL")
130
+ api_key = api_key or os.getenv("UTU_LLM_API_KEY")
131
+ if not api_key or not base_url:
132
+ raise ValueError("UTU_LLM_API_KEY and UTU_LLM_BASE_URL must be set")
133
+ openai_client = AsyncOpenAI(
134
+ api_key=api_key,
135
+ base_url=base_url,
136
+ timeout=100,
137
+ )
138
+ if type == "chat.completions":
139
+ return OpenAIChatCompletionsModel(model=model, openai_client=openai_client)
140
+ elif type == "responses":
141
+ return OpenAIResponsesModel(model=model, openai_client=openai_client)
142
+ else:
143
+ raise ValueError("Invalid type: " + type)
144
+
145
+ @staticmethod
146
+ def get_trajectory_from_agent_result(agent_result: RunResult, agent_name: str = None) -> dict:
147
+ if agent_name is None:
148
+ agent_name = agent_result.last_agent.name
149
+ return {
150
+ "agent": agent_name,
151
+ "trajectory": ChatCompletionConverter.items_to_messages(agent_result.to_input_list()),
152
+ }
153
+
154
+ @staticmethod
155
+ def print_new_items(new_items: list[RunItem]) -> None:
156
+ """Print new items generated by Runner.run()"""
157
+ for new_item in new_items:
158
+ agent_name = new_item.agent.name
159
+ if isinstance(new_item, MessageOutputItem):
160
+ PrintUtils.print_bot(f"{agent_name}: {ItemHelpers.text_message_output(new_item)}")
161
+ elif isinstance(new_item, HandoffOutputItem):
162
+ PrintUtils.print_info(f"Handed off from {new_item.source_agent.name} to {new_item.target_agent.name}")
163
+ elif isinstance(new_item, ToolCallItem):
164
+ assert isinstance(new_item.raw_item, ResponseFunctionToolCall) # DONOT use openai's built-in tools
165
+ PrintUtils.print_info(
166
+ f"{agent_name}: Calling a tool: {new_item.raw_item.name}({json.loads(new_item.raw_item.arguments)})"
167
+ )
168
+ elif isinstance(new_item, ToolCallOutputItem):
169
+ PrintUtils.print_tool(f"Tool call output: {new_item.output}")
170
+ elif isinstance(new_item, ReasoningItem):
171
+ PrintUtils.print_info(f"{agent_name}: Reasoning: {new_item.raw_item}")
172
+ else:
173
+ PrintUtils.print_info(f"{agent_name}: Skipping item: {new_item.__class__.__name__}")
174
+
175
+ @staticmethod
176
+ async def print_stream_events(result: AsyncIterator[StreamEvent]) -> None:
177
+ """Print stream events generated by Runner.run_streamed()"""
178
+ async for event in result:
179
+ # print(f"> [DEBUG] event: {event}")
180
+ if isinstance(event, RawResponsesStreamEvent):
181
+ # event.data -- ResponseStreamEvent
182
+ if event.data.type == "response.output_item.added":
183
+ match event.data.item.type:
184
+ # computer_call, code_interpreter_call, custom_tool_call, file_search_call, function_call,
185
+ # we_search_call, image_generation_call, local_shell_call,
186
+ # mcp_call, mcp_list_tools, mcp_approval_request, message, reasoning
187
+ case "message":
188
+ pass
189
+ case "function_call":
190
+ PrintUtils.print_bot(
191
+ f"<toolcall name={event.data.item.name}>{event.data.item.arguments}", end=""
192
+ )
193
+ case _:
194
+ PrintUtils.print_bot(f"<{event.data.item.type}>", end="")
195
+ elif event.data.type == "response.output_item.done":
196
+ match event.data.item.type:
197
+ case "message":
198
+ pass
199
+ # PrintUtils.print_bot("") # add a new line?
200
+ case "function_call":
201
+ PrintUtils.print_bot("</toolcall>")
202
+ case _:
203
+ # PrintUtils.print_bot(f"</{event.data.item.type}>")
204
+ logger.info(f"</{event.data.item.type}>") # It seems that vllm's output order is wrong
205
+ elif event.data.type == "response.content_part.added":
206
+ match event.data.part.type:
207
+ # output_text, refusal
208
+ case "output_text":
209
+ pass
210
+ case "refusal":
211
+ PrintUtils.print_bot(f"<refusal>{event.data.part.refusal}", end="")
212
+ case _:
213
+ logger.warning(f"Unknown part type: {event.data.part.type}! {event}")
214
+ elif event.data.type == "response.content_part.done":
215
+ match event.data.part.type:
216
+ case "output_text":
217
+ pass
218
+ case "refusal":
219
+ PrintUtils.print_bot("</refusal>")
220
+ case _:
221
+ logger.warning(f"Unknown part type: {event.data.part.type}! {event}")
222
+ elif event.data.type == "response.reasoning_summary_part.added":
223
+ PrintUtils.print_info("<reasoning_summary>", end="")
224
+ elif event.data.type == "response.reasoning_summary_part.done":
225
+ # PrintUtils.print_info("</reasoning_summary>", end="")
226
+ logger.info("</reasoning_summary>") # It seems that vllm's output order is wrong
227
+ elif event.data.type == "response.reasoning_summary_text.delta":
228
+ PrintUtils.print_info(f"{event.data.delta}", end="")
229
+ elif event.data.type == "response.function_call_arguments.delta":
230
+ PrintUtils.print_bot(f"{event.data.delta}", end="")
231
+ elif event.data.type == "response.function_call_arguments.done":
232
+ pass
233
+ elif event.data.type == "response.output_text.delta":
234
+ PrintUtils.print_bot(f"{event.data.delta}", end="")
235
+ elif event.data.type == "response.reasoning_text.delta":
236
+ PrintUtils.print_info(f"{event.data.delta}", end="")
237
+ elif event.data.type == "response.reasoning_text.done":
238
+ PrintUtils.print_info("</reasoning_text>", end="")
239
+ elif event.data.type in ("response.output_text.done",):
240
+ PrintUtils.print_info("")
241
+ elif event.data.type in (
242
+ "response.created",
243
+ "response.completed",
244
+ "response.in_progress",
245
+ ):
246
+ pass
247
+ else:
248
+ PrintUtils.print_info(f"Unknown event type: {event.data.type}! {event}")
249
+ # raise ValueError(f"Unknown event type: {event.data.type}")
250
+ elif isinstance(event, RunItemStreamEvent):
251
+ item: RunItem = event.item
252
+ if item.type == "message_output_item":
253
+ pass # do not print twice to avoid duplicate! (already handled `response.output_text.delta`)
254
+ # PrintUtils.print_bot(f"{ItemHelpers.text_message_output(item).strip()}")
255
+ elif item.type == "reasoning_item":
256
+ pass
257
+ elif item.type == "tool_call_item":
258
+ pass
259
+ # PrintUtils.print_bot([tool_call] {item.raw_item.name}({item.raw_item.arguments})")
260
+ elif item.type == "tool_call_output_item":
261
+ PrintUtils.print_tool(f"[tool_output] {item.output}") # item.raw_item
262
+ elif item.type == "handoff_call_item": # same as `ToolCallItem`
263
+ PrintUtils.print_bot(f"[handoff_call] {item.raw_item.name}({item.raw_item.arguments})")
264
+ elif item.type == "handoff_output_item":
265
+ PrintUtils.print_info(f">> Handoff from {item.source_agent.name} to {item.target_agent.name}")
266
+ elif event.type in (
267
+ "mcp_list_tools_item",
268
+ "mcp_approval_request_item",
269
+ "mcp_approval_response_item",
270
+ ):
271
+ PrintUtils.print_info(f" >>> Skipping item: {event}")
272
+ else:
273
+ PrintUtils.print_info(f" >>> Skipping item: {item.__class__.__name__}")
274
+ elif isinstance(event, AgentUpdatedStreamEvent):
275
+ PrintUtils.print_info(f">> new agent: {event.new_agent.name}")
276
+ # skip events from youtu-agent
277
+ elif event.type in ("orchestrator_stream_event", "orchestra_stream_event"):
278
+ pass
279
+ else:
280
+ logger.warning(f"Unknown event type: {event.type}! {event}")
281
+ print() # Newline after stream?
282
+
283
+ @staticmethod
284
+ def convert_model_settings(params: OpenAIChatCompletionParams) -> ModelSettings:
285
+ # "tools", "messages", "model"
286
+ # FIXME: move to extra_args
287
+ for p in ("max_completion_tokens", "top_logprobs", "logprobs", "seed", "stop"):
288
+ if p in params:
289
+ logger.warning(f"Parameter `{p}` is not supported in ModelSettings")
290
+ return ModelSettings(
291
+ max_tokens=params.get("max_tokens", None),
292
+ temperature=params.get("temperature", None),
293
+ top_p=params.get("top_p", None),
294
+ frequency_penalty=params.get("frequency_penalty", None),
295
+ presence_penalty=params.get("presence_penalty", None),
296
+ tool_choice=params.get("tool_choice", None),
297
+ parallel_tool_calls=params.get("parallel_tool_calls", None),
298
+ extra_query=params.get("extra_query", None),
299
+ extra_body=params.get("extra_body", None),
300
+ extra_headers=params.get("extra_headers", None),
301
+ )
302
+
303
+ @staticmethod
304
+ def convert_sp_input(
305
+ messages: list[ChatCompletionMessageParam],
306
+ ) -> tuple[str | None, str | list[TResponseInputItem]]:
307
+ if isinstance(messages, str):
308
+ return None, messages
309
+ if messages[0].get("role", None) == "system":
310
+ return messages[0]["content"], messages[1:]
311
+ return None, messages
312
+
313
+ @staticmethod
314
+ def convert_tool(tool: ChatCompletionToolParam) -> FunctionTool:
315
+ assert tool["type"] == "function"
316
+ return FunctionTool(
317
+ name=tool["function"]["name"],
318
+ description=tool["function"].get("description", ""),
319
+ params_json_schema=tool["function"].get("parameters", None),
320
+ on_invoke_tool=None,
321
+ )
322
+
323
+ @staticmethod
324
+ def get_message_from_image(image_url: str) -> dict:
325
+ """Get a message dict for image input."""
326
+ # from openai.types.responses.response_input_item_param import Message
327
+ # from openai.types.responses.response_input_image_param import ResponseInputImageParam
328
+ return {"role": "user", "content": [{"type": "input_image", "image_url": encode_image(image_url)}]}
329
+
330
+
331
+ class SimplifiedOpenAIChatCompletionsModel(OpenAIChatCompletionsModel):
332
+ """extend OpenAIChatCompletionsModel to support basic api
333
+ - enable tracing based on SimplifiedAsyncOpenAI
334
+ """
335
+
336
+ async def query_one(self, **kwargs) -> str:
337
+ system_instructions, input = AgentsUtils.convert_sp_input(kwargs["messages"])
338
+ model_settings = AgentsUtils.convert_model_settings(kwargs)
339
+ tools = [AgentsUtils.convert_tool(tool) for tool in kwargs.get("tools", [])]
340
+ response = await self.get_response(
341
+ system_instructions=system_instructions,
342
+ input=input,
343
+ model_settings=model_settings,
344
+ tools=tools,
345
+ output_schema=None,
346
+ handoffs=[],
347
+ tracing=ModelTracing.ENABLED,
348
+ previous_response_id=None,
349
+ prompt=None,
350
+ )
351
+ return ChatCompletionConverter.items_to_messages(response.to_input_items())
352
+ # with generation_span(
353
+ # model=kwargs["model"],
354
+ # model_config=_model_settings,
355
+ # input=_messages,
356
+ # ) as span_generation:
357
+ # result = await self.chat.completions.create(**kwargs)
358
+ # span_generation.span_data.output = result.choices[0].message.model_dump()
359
+ # return result
utils/common.py ADDED
@@ -0,0 +1,82 @@
1
+ import asyncio
2
+ import importlib.util
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+ from .path import DIR_ROOT
9
+
10
+
11
+ def get_event_loop() -> asyncio.AbstractEventLoop:
12
+ try:
13
+ loop = asyncio.get_event_loop()
14
+ except RuntimeError:
15
+ loop = asyncio.new_event_loop()
16
+ asyncio.set_event_loop(loop)
17
+ return loop
18
+
19
+
20
+ def schema_to_basemodel(schema: dict, class_name: str = None) -> type[BaseModel]:
21
+ type_map = {
22
+ "string": str,
23
+ "integer": int,
24
+ "number": float,
25
+ "boolean": bool,
26
+ }
27
+
28
+ def get_python_type(prop_schema):
29
+ prop_type = prop_schema.get("type")
30
+
31
+ if prop_type == "array":
32
+ item_type = prop_schema.get("items", {}).get("type", "string")
33
+ return list[type_map.get(item_type, str)]
34
+
35
+ return type_map.get(prop_type, str)
36
+
37
+ annotations = {}
38
+ fields = {}
39
+ properties = schema.get("properties", {})
40
+ required_fields = set(schema.get("required", []))
41
+
42
+ for field_name, field_schema in properties.items():
43
+ annotations[field_name] = get_python_type(field_schema)
44
+ field_kwargs = {}
45
+ if "description" in field_schema:
46
+ field_kwargs["description"] = field_schema["description"]
47
+ if field_name not in required_fields:
48
+ field_kwargs["default"] = None
49
+ annotations[field_name] = annotations[field_name] | None
50
+ if field_kwargs:
51
+ fields[field_name] = Field(**field_kwargs)
52
+ attrs = {
53
+ "__annotations__": annotations,
54
+ "__module__": __name__,
55
+ }
56
+ attrs.update(fields)
57
+
58
+ class_name = class_name or schema.get("title", "GeneratedModel")
59
+ ModelClass = type(class_name, (BaseModel,), attrs)
60
+ return ModelClass
61
+
62
+
63
+ def load_class_from_file(filepath: str, class_name: str) -> type:
64
+ """Load class from file."""
65
+ if not filepath.startswith("/"):
66
+ filepath = str(DIR_ROOT / filepath)
67
+
68
+ filepath = Path(filepath).absolute()
69
+ module_name = filepath.stem
70
+ spec = importlib.util.spec_from_file_location(module_name, filepath)
71
+ if spec is None:
72
+ raise ImportError(f"Could not load spec from file '{filepath}'")
73
+
74
+ module = importlib.util.module_from_spec(spec)
75
+ sys.modules[module_name] = module
76
+
77
+ spec.loader.exec_module(module)
78
+
79
+ if hasattr(module, class_name):
80
+ return getattr(module, class_name)
81
+ else:
82
+ raise AttributeError(f"Class '{class_name}' not found in module '{module_name}'")
utils/env.py ADDED
@@ -0,0 +1,34 @@
1
+ import importlib.metadata
2
+ import os
3
+
4
+ from dotenv import find_dotenv, load_dotenv
5
+
6
+ load_dotenv(find_dotenv(raise_error_if_not_found=True), verbose=True, override=True)
7
+
8
+
9
+ class EnvUtils:
10
+ @staticmethod
11
+ def get_env(key: str, default: str | None = None) -> str | None:
12
+ """Get the value of an environment variable. Raise an error if default is not privided (None)"""
13
+ if default is None:
14
+ res = os.getenv(key)
15
+ if res is None:
16
+ raise ValueError(f"Environment variable {key} is not set")
17
+ return res
18
+ return os.getenv(key, default)
19
+
20
+ @staticmethod
21
+ def assert_env(key: str | list[str]) -> None:
22
+ if isinstance(key, list):
23
+ for k in key:
24
+ EnvUtils.assert_env(k)
25
+ else:
26
+ if not os.getenv(key):
27
+ raise ValueError(f"Environment variable {key} is not set")
28
+
29
+ @staticmethod
30
+ def ensure_package(package_name: str) -> None:
31
+ try:
32
+ importlib.metadata.version(package_name)
33
+ except importlib.metadata.PackageNotFoundError:
34
+ raise ValueError(f"Package {package_name} is required but not installed!") from None
utils/image.py ADDED
@@ -0,0 +1,16 @@
1
+ import base64
2
+ import pathlib
3
+
4
+ import requests
5
+
6
+
7
+ def encode_image(image_path: str) -> str:
8
+ """Encode an image from a local path or URL to a base64 string."""
9
+ if pathlib.Path(image_path).is_file():
10
+ with open(image_path, "rb") as image_file:
11
+ return base64.b64encode(image_file.read()).decode("utf-8")
12
+ else:
13
+ response = requests.get(image_path)
14
+ response.raise_for_status()
15
+ image_base64 = base64.b64encode(response.content).decode("utf-8")
16
+ return f"data:image/jpeg;base64,{image_base64}"
@@ -0,0 +1,64 @@
1
+ import json
2
+ import re
3
+
4
+ LANGUAGE_TO_CANDS = {
5
+ "python": ["py", "python"],
6
+ "json": ["json"],
7
+ "javascript": ["js", "javascript"],
8
+ "yaml": ["yaml", "yml"],
9
+ }
10
+
11
+
12
+ class LLMOutputParser:
13
+ @staticmethod
14
+ def extract_code_blocks(s: str) -> list[tuple[str, str]]:
15
+ """Parse all code blocks (language, code) from the given string."""
16
+ pattern = re.compile(r"```(\w*)\n(.*?)```", re.DOTALL)
17
+ code_blocks = []
18
+ for match in pattern.finditer(s):
19
+ language = match.group(1).strip()
20
+ code = match.group(2).strip()
21
+ code_blocks.append((language, code))
22
+ return code_blocks
23
+
24
+ @staticmethod
25
+ def extract_code_block_with_language(s: str, language: str) -> str:
26
+ code_blocks = LLMOutputParser.extract_code_blocks(s)
27
+ if not code_blocks:
28
+ return s # fallback
29
+ for lang, code in code_blocks:
30
+ if lang.lower() in LANGUAGE_TO_CANDS[language]:
31
+ return code
32
+ return code_blocks[0][1] # fallback
33
+
34
+ @staticmethod
35
+ def extract_code_python(s: str) -> str:
36
+ """Extract the first code block with the given language from the given string."""
37
+ return LLMOutputParser.extract_code_block_with_language(s, "python")
38
+
39
+ @staticmethod
40
+ def extract_code_json(s: str, try_parse: bool = True) -> str | dict:
41
+ """Extract the first json code block from the given string."""
42
+ code = LLMOutputParser.extract_code_block_with_language(s, "json")
43
+ if try_parse:
44
+ try:
45
+ return json.loads(code)
46
+ except json.JSONDecodeError:
47
+ return code
48
+ return code
49
+
50
+ @staticmethod
51
+ def camel_to_snake(name: str) -> str:
52
+ """Convert CamelCase / PascalCase to snake_case."""
53
+ s1 = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", name)
54
+ s2 = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s1)
55
+ return s2.lower()
56
+
57
+ @staticmethod
58
+ def snake_to_camel(name: str, pascal: bool = True) -> str:
59
+ """Convert snake_case to CamelCase / PascalCase."""
60
+ components = name.split("_")
61
+ if pascal:
62
+ return "".join(x.title() for x in components)
63
+ else:
64
+ return components[0].lower() + "".join(x.title() for x in components[1:])
utils/log.py ADDED
@@ -0,0 +1,79 @@
1
+ import json
2
+ import logging
3
+ import pathlib
4
+ from logging.handlers import TimedRotatingFileHandler
5
+ from typing import Literal
6
+
7
+ from colorlog import ColoredFormatter
8
+
9
+ DIR_LOGS = pathlib.Path(__file__).parent.parent.parent / "logs"
10
+ DIR_LOGS.mkdir(exist_ok=True)
11
+
12
+ # Flag to track if logging has been set up
13
+ _LOGGING_INITIALIZED = False
14
+
15
+
16
+ def setup_logging(level: Literal["WARNING", "INFO", "DEBUG"] = "WARNING") -> None:
17
+ # Check if logging has already been initialized
18
+ global _LOGGING_INITIALIZED
19
+ if _LOGGING_INITIALIZED:
20
+ logging.getLogger().warning("Logging has already been initialized! Skipping...")
21
+ return
22
+
23
+ utu_logger = logging.getLogger("utu")
24
+ utu_logger.setLevel(logging.DEBUG)
25
+ if utu_logger.handlers:
26
+ utu_logger.handlers.clear()
27
+
28
+ console_handler = logging.StreamHandler()
29
+ console_handler.setLevel(level)
30
+ color_formatter = ColoredFormatter(
31
+ "%(green)s%(asctime)s%(reset)s[%(blue)s%(name)s%(reset)s] - "
32
+ "%(log_color)s%(levelname)s%(reset)s - %(filename)s:%(lineno)d - %(green)s%(message)s%(reset)s",
33
+ # " - %(cyan)s%(threadName)s%(reset)s",
34
+ log_colors={
35
+ "DEBUG": "cyan",
36
+ "INFO": "white",
37
+ "WARNING": "yellow",
38
+ "ERROR": "red",
39
+ "CRITICAL": "red,bg_white",
40
+ },
41
+ secondary_log_colors={"asctime": {"green": "green"}, "name": {"blue": "blue"}},
42
+ )
43
+ console_handler.setFormatter(color_formatter)
44
+
45
+ file_handler = TimedRotatingFileHandler(
46
+ DIR_LOGS / "utu.log", when="midnight", interval=1, backupCount=30, encoding="utf-8"
47
+ )
48
+ file_handler.setLevel(logging.DEBUG)
49
+ formatter = logging.Formatter(
50
+ "%(asctime)s[%(name)s] - %(levelname)s - %(filename)s:%(lineno)d - %(message)s - %(threadName)s"
51
+ )
52
+ file_handler.setFormatter(formatter)
53
+
54
+ utu_logger.addHandler(console_handler)
55
+ utu_logger.addHandler(file_handler)
56
+ utu_logger.info(f"Logging initialized with level {level}.")
57
+
58
+ # Mark logging as initialized
59
+ _LOGGING_INITIALIZED = True
60
+
61
+
62
+ def get_logger(name: str, level: int | str = logging.DEBUG) -> logging.Logger:
63
+ logger = logging.getLogger(name)
64
+ logger.setLevel(level)
65
+
66
+ def log_error_with_exc(msg, *args, **kwargs):
67
+ kwargs["exc_info"] = True
68
+ logger.error(msg, *args, **kwargs)
69
+
70
+ logger.error_exc = log_error_with_exc
71
+ return logger
72
+
73
+
74
+ def oneline_object(obj: object, limit: int = 100) -> str:
75
+ try:
76
+ s = json.dumps(obj, ensure_ascii=False)
77
+ except TypeError:
78
+ s = json.dumps(str(obj), ensure_ascii=False)
79
+ return f"{s[:limit]}..." if len(s) > limit else s
@@ -0,0 +1,5 @@
1
+ from .openai_utils import OpenAIUtils
2
+ from .simplified_client import SimplifiedAsyncOpenAI
3
+ from .types import OpenAIChatCompletionParams, OpenAIResponsesParams
4
+
5
+ __all__ = ["OpenAIUtils", "SimplifiedAsyncOpenAI", "OpenAIChatCompletionParams", "OpenAIResponsesParams"]