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 +35 -0
- utils/agents_utils.py +359 -0
- utils/common.py +82 -0
- utils/env.py +34 -0
- utils/image.py +16 -0
- utils/llm_output_parser.py +64 -0
- utils/log.py +79 -0
- utils/openai_utils/__init__.py +5 -0
- utils/openai_utils/openai_utils.py +147 -0
- utils/openai_utils/simplified_client.py +119 -0
- utils/openai_utils/types.py +66 -0
- utils/path.py +130 -0
- utils/print_utils.py +112 -0
- utils/script_utils.py +22 -0
- utils/sqlmodel_utils.py +95 -0
- utils/token.py +21 -0
- utils/tool_cache.py +121 -0
- youtu_agent-0.1.2.dist-info/METADATA +439 -0
- youtu_agent-0.1.2.dist-info/RECORD +21 -0
- youtu_agent-0.1.2.dist-info/WHEEL +4 -0
- youtu_agent-0.1.2.dist-info/licenses/LICENSE +28 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from openai._streaming import AsyncStream
|
|
5
|
+
from openai.types.chat import (
|
|
6
|
+
ChatCompletionChunk,
|
|
7
|
+
ChatCompletionMessage,
|
|
8
|
+
ChatCompletionMessageFunctionToolCall,
|
|
9
|
+
ChatCompletionMessageToolCall,
|
|
10
|
+
ChatCompletionToolParam,
|
|
11
|
+
)
|
|
12
|
+
from openai.types.responses import (
|
|
13
|
+
FunctionToolParam,
|
|
14
|
+
Response,
|
|
15
|
+
ResponseStreamEvent,
|
|
16
|
+
)
|
|
17
|
+
from pydantic import BaseModel
|
|
18
|
+
|
|
19
|
+
from ..print_utils import PrintUtils
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class OpenAIUtils:
|
|
25
|
+
# --------------------------------------------------------
|
|
26
|
+
# chat completions
|
|
27
|
+
# --------------------------------------------------------
|
|
28
|
+
@staticmethod
|
|
29
|
+
def print_message(message: ChatCompletionMessage) -> None:
|
|
30
|
+
if hasattr(message, "reasoning_content") and message.reasoning_content:
|
|
31
|
+
PrintUtils.print_info(f"{message.reasoning_content}")
|
|
32
|
+
if message.content:
|
|
33
|
+
PrintUtils.print_bot(f"{message.content}", add_prefix=True)
|
|
34
|
+
if message.tool_calls:
|
|
35
|
+
for tool_call in message.tool_calls:
|
|
36
|
+
PrintUtils.print_bot(f"<{tool_call.function.name}>{tool_call.function.arguments}", add_prefix=True)
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
async def print_stream(stream: AsyncStream[ChatCompletionChunk]) -> ChatCompletionMessage:
|
|
40
|
+
final_tool_calls: dict[int, ChatCompletionMessageToolCall] = {}
|
|
41
|
+
content = ""
|
|
42
|
+
async for chunk in stream:
|
|
43
|
+
delta = chunk.choices[0].delta
|
|
44
|
+
if hasattr(delta, "reasoning_content") and delta.reasoning_content:
|
|
45
|
+
PrintUtils.print_info(f"{delta.reasoning_content}", end="", color="green")
|
|
46
|
+
if delta.content:
|
|
47
|
+
content += delta.content
|
|
48
|
+
PrintUtils.print_info(f"{delta.content}", end="", color="gray")
|
|
49
|
+
if delta.tool_calls:
|
|
50
|
+
for tool_call in delta.tool_calls:
|
|
51
|
+
index = tool_call.index
|
|
52
|
+
if index not in final_tool_calls:
|
|
53
|
+
final_tool_calls[index] = tool_call
|
|
54
|
+
PrintUtils.print_info(
|
|
55
|
+
f"<{tool_call.function.name}>{tool_call.function.arguments}", end="", color="blue"
|
|
56
|
+
)
|
|
57
|
+
else:
|
|
58
|
+
if final_tool_calls[index].function.arguments:
|
|
59
|
+
final_tool_calls[index].function.arguments += tool_call.function.arguments
|
|
60
|
+
else:
|
|
61
|
+
final_tool_calls[index].function.arguments = tool_call.function.arguments
|
|
62
|
+
PrintUtils.print_info(f"{tool_call.function.arguments}", end="", color="blue")
|
|
63
|
+
PrintUtils.print_info("") # print a newline
|
|
64
|
+
tool_calls = [
|
|
65
|
+
ChatCompletionMessageFunctionToolCall(
|
|
66
|
+
id=tool_call.id,
|
|
67
|
+
function=tool_call.function.model_dump(),
|
|
68
|
+
type=tool_call.type, # type is always "function"
|
|
69
|
+
)
|
|
70
|
+
for tool_call in final_tool_calls.values()
|
|
71
|
+
]
|
|
72
|
+
message = ChatCompletionMessage(role="assistant", content=content, tool_calls=tool_calls)
|
|
73
|
+
OpenAIUtils.print_message(message)
|
|
74
|
+
return message
|
|
75
|
+
|
|
76
|
+
# --------------------------------------------------------
|
|
77
|
+
# responses
|
|
78
|
+
# --------------------------------------------------------
|
|
79
|
+
@staticmethod
|
|
80
|
+
def print_response(response: Response) -> None:
|
|
81
|
+
for item in response.output:
|
|
82
|
+
# print(f"> responses item: {item}")
|
|
83
|
+
match item.type:
|
|
84
|
+
case "reasoning":
|
|
85
|
+
content = getattr(item, "content", item.summary)
|
|
86
|
+
PrintUtils.print_bot(f"<reasoning>{content}</reasoning>", add_prefix=True, color="gray")
|
|
87
|
+
case "message":
|
|
88
|
+
PrintUtils.print_bot(f"{item.content}", add_prefix=True)
|
|
89
|
+
case "function_call":
|
|
90
|
+
PrintUtils.print_info(f"<{item.name}>({item.arguments})")
|
|
91
|
+
case "file_search_call":
|
|
92
|
+
PrintUtils.print_info(f"<{item.type}>({item.queries})")
|
|
93
|
+
case "web_search_call":
|
|
94
|
+
PrintUtils.print_info(f"<{item.type}>({item.action})")
|
|
95
|
+
case "computer_call":
|
|
96
|
+
PrintUtils.print_info(f"<{item.type}>({item.action})")
|
|
97
|
+
case "image_generation_call":
|
|
98
|
+
PrintUtils.print_info(f"<{item.type}> -> {item.result[:4]}")
|
|
99
|
+
case "code_interpreter_call":
|
|
100
|
+
PrintUtils.print_info(
|
|
101
|
+
f"<{item.type}>(container_id={item.container_id}, code={item.code}) -> {item.outputs}"
|
|
102
|
+
)
|
|
103
|
+
case "local_shell_call":
|
|
104
|
+
PrintUtils.print_info(f"<{item.type}>(action={item.action})")
|
|
105
|
+
case "mcp_list_tools":
|
|
106
|
+
PrintUtils.print_info(f"<{item.type}>(server={item.server_label}) -> {item.tools}")
|
|
107
|
+
case "mcp_call":
|
|
108
|
+
PrintUtils.print_info(
|
|
109
|
+
f"<{item.type}>(server={item.server_label}) {item.name}({item.arguments}) -> {item.output}"
|
|
110
|
+
)
|
|
111
|
+
case "mcp_approval_request":
|
|
112
|
+
PrintUtils.print_info(f"<{item.type}>(server={item.server_label}) {item.name}({item.arguments})")
|
|
113
|
+
case _:
|
|
114
|
+
PrintUtils.print_error(f"Unknown item type: {item.type}\n{item}")
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def print_response_stream(stream: AsyncStream[ResponseStreamEvent]) -> Response:
|
|
118
|
+
raise NotImplementedError
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def get_response_configs(response: Response, include_output: bool = False) -> dict:
|
|
122
|
+
"""Get response configs from response"""
|
|
123
|
+
data = response.model_dump()
|
|
124
|
+
if not include_output:
|
|
125
|
+
del data["output"]
|
|
126
|
+
return data
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def get_response_output(response: Response) -> list[dict]:
|
|
130
|
+
"""Get response output from response"""
|
|
131
|
+
return response.model_dump()["output"]
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def tool_chatcompletion_to_responses(cls, tool: ChatCompletionToolParam) -> FunctionToolParam:
|
|
135
|
+
assert tool["type"] == "function"
|
|
136
|
+
return FunctionToolParam(
|
|
137
|
+
name=tool["function"]["name"],
|
|
138
|
+
description=tool["function"].get("description", ""),
|
|
139
|
+
parameters=tool["function"].get("parameters", None),
|
|
140
|
+
type="function",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def maybe_basemodel_to_dict(obj: Any) -> dict | None:
|
|
145
|
+
if isinstance(obj, BaseModel):
|
|
146
|
+
return obj.model_dump()
|
|
147
|
+
return obj
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from openai import AsyncOpenAI, AsyncStream
|
|
6
|
+
from openai.types.chat import ChatCompletion, ChatCompletionChunk
|
|
7
|
+
from openai.types.responses import Response, ResponseStreamEvent
|
|
8
|
+
|
|
9
|
+
from .types import (
|
|
10
|
+
OpenAIChatCompletionParams,
|
|
11
|
+
OpenAIChatCompletionParamsKeys,
|
|
12
|
+
OpenAIResponsesParams,
|
|
13
|
+
OpenAIResponsesParamsKeys,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SimplifiedAsyncOpenAI(AsyncOpenAI):
|
|
20
|
+
"""Simplified OpenAI client for chat.completions and responses API, with default config"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
*,
|
|
25
|
+
type: Literal["chat.completions", "responses"] = None,
|
|
26
|
+
# openai client kwargs
|
|
27
|
+
api_key: str | None = None,
|
|
28
|
+
base_url: str | None = None,
|
|
29
|
+
# default configs
|
|
30
|
+
**kwargs: dict,
|
|
31
|
+
) -> None:
|
|
32
|
+
logger.info(f"> type: {type}, base_url: {base_url}, kwargs: {kwargs}")
|
|
33
|
+
super().__init__(
|
|
34
|
+
api_key=api_key or os.getenv("UTU_LLM_API_KEY") or "xxx", base_url=base_url or os.getenv("UTU_LLM_BASE_URL")
|
|
35
|
+
)
|
|
36
|
+
self.type = type or os.getenv("UTU_LLM_TYPE", "chat.completions")
|
|
37
|
+
self.type_create_params = (
|
|
38
|
+
OpenAIChatCompletionParamsKeys if self.type == "chat.completions" else OpenAIResponsesParamsKeys
|
|
39
|
+
)
|
|
40
|
+
self.default_config = self._process_kwargs(kwargs)
|
|
41
|
+
|
|
42
|
+
def _process_kwargs(self, kwargs: dict) -> dict:
|
|
43
|
+
# parse kwargs for ChatCompletionParams
|
|
44
|
+
default_config = {}
|
|
45
|
+
for k, v in kwargs.items():
|
|
46
|
+
if k in self.type_create_params:
|
|
47
|
+
default_config[k] = v
|
|
48
|
+
default_config["model"] = default_config.get("model", os.getenv("UTU_LLM_MODEL"))
|
|
49
|
+
return default_config
|
|
50
|
+
|
|
51
|
+
async def query_one(self, **kwargs) -> str:
|
|
52
|
+
"""Simplified chat.complete / responses API
|
|
53
|
+
WARNING: Only for basic text i/o usage! You should not use the method with querying with customized configs!
|
|
54
|
+
"""
|
|
55
|
+
if "stream" in kwargs:
|
|
56
|
+
assert kwargs["stream"] is False, "stream is not supported in `query_one`"
|
|
57
|
+
|
|
58
|
+
if self.type == "chat.completions":
|
|
59
|
+
chat_completion: ChatCompletion = await self.chat_completions_create(**kwargs)
|
|
60
|
+
return chat_completion.choices[0].message.content
|
|
61
|
+
elif self.type == "responses":
|
|
62
|
+
response: Response = await self.responses_create(**kwargs)
|
|
63
|
+
return response.output_text # NOTE: will not return toolcall or reasoning
|
|
64
|
+
else:
|
|
65
|
+
raise ValueError(f"Unknown type: {self.type}")
|
|
66
|
+
|
|
67
|
+
async def chat_completions_create(self, **kwargs) -> ChatCompletion | AsyncStream[ChatCompletionChunk]:
|
|
68
|
+
assert self.type == "chat.completions", "`chat_completions_create` is not supported for responses API"
|
|
69
|
+
unknown_params = self.check_known_keys(kwargs, self.type_create_params)
|
|
70
|
+
if unknown_params:
|
|
71
|
+
logger.warning(f"Unknown parameters: {unknown_params} for {self.type} API!")
|
|
72
|
+
kwargs = self.process_chat_completion_params(kwargs, self.default_config)
|
|
73
|
+
return await self.chat.completions.create(**kwargs)
|
|
74
|
+
|
|
75
|
+
async def responses_create(self, **kwargs) -> Response | AsyncStream[ResponseStreamEvent]:
|
|
76
|
+
unknown_params = self.check_known_keys(kwargs, self.type_create_params)
|
|
77
|
+
if unknown_params - {"messages"}: # ignore
|
|
78
|
+
logger.warning(f"Unknown parameters: {unknown_params} for {self.type} API!")
|
|
79
|
+
assert self.type == "responses", "`responses_create` is not supported for chat.completions API"
|
|
80
|
+
kwargs = self.process_responses_params(kwargs, self.default_config)
|
|
81
|
+
return await self.responses.create(**kwargs)
|
|
82
|
+
|
|
83
|
+
def process_chat_completion_params(
|
|
84
|
+
self, kwargs: OpenAIChatCompletionParams, default_config: OpenAIChatCompletionParams
|
|
85
|
+
) -> OpenAIChatCompletionParams:
|
|
86
|
+
"""Process chat completion params, convert str to list of messages, merge default config"""
|
|
87
|
+
assert "messages" in kwargs
|
|
88
|
+
if isinstance(kwargs["messages"], str):
|
|
89
|
+
kwargs["messages"] = [{"role": "user", "content": kwargs["messages"]}]
|
|
90
|
+
return self._merge_default_config(kwargs, default_config)
|
|
91
|
+
|
|
92
|
+
def process_responses_params(
|
|
93
|
+
self, kwargs: OpenAIResponsesParams, default_config: OpenAIResponsesParams
|
|
94
|
+
) -> OpenAIResponsesParams:
|
|
95
|
+
"""Process responses params, convert str to list of messages, merge default config"""
|
|
96
|
+
if "input" not in kwargs:
|
|
97
|
+
# try parse query for chat.completions
|
|
98
|
+
assert "messages" in kwargs
|
|
99
|
+
input = kwargs.pop("messages")
|
|
100
|
+
if isinstance(input, str):
|
|
101
|
+
kwargs["input"] = [{"role": "user", "content": input}]
|
|
102
|
+
else:
|
|
103
|
+
kwargs["input"] = input
|
|
104
|
+
else:
|
|
105
|
+
if isinstance(kwargs["input"], str):
|
|
106
|
+
kwargs["input"] = [{"role": "user", "content": kwargs["input"]}]
|
|
107
|
+
return self._merge_default_config(kwargs, default_config)
|
|
108
|
+
|
|
109
|
+
def _merge_default_config(self, kwargs: dict, default_config: dict) -> dict:
|
|
110
|
+
"""Merge default config"""
|
|
111
|
+
for k, v in default_config.items():
|
|
112
|
+
if k not in kwargs:
|
|
113
|
+
kwargs[k] = v
|
|
114
|
+
return kwargs
|
|
115
|
+
|
|
116
|
+
def check_known_keys(self, kwargs: dict, known_keys: set[str]) -> set:
|
|
117
|
+
"""Check if all keys in kwargs are in known_keys"""
|
|
118
|
+
unknown_keys = set(kwargs.keys()) - known_keys
|
|
119
|
+
return unknown_keys
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
from typing import Literal, TypedDict
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
from openai._types import NOT_GIVEN, Body, Headers, NotGiven, Query
|
|
6
|
+
from openai.types.chat import ChatCompletionMessageParam, ChatCompletionToolParam
|
|
7
|
+
from openai.types.chat.completion_create_params import ResponseFormat
|
|
8
|
+
from openai.types.responses import ResponseInputParam, ResponseTextConfigParam, ToolParam
|
|
9
|
+
from openai.types.responses.response_create_params import ToolChoice
|
|
10
|
+
from openai.types.shared import ChatModel, Reasoning, ReasoningEffort, ResponsesModel
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OpenAICreateBaseParams(TypedDict):
|
|
14
|
+
stream: bool | None = False
|
|
15
|
+
# from openai.resources.chat.completions.Completions.create
|
|
16
|
+
extra_headers: Headers | None = None
|
|
17
|
+
extra_query: Query | None = None
|
|
18
|
+
extra_body: Body | None = None
|
|
19
|
+
timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# CompletionCreateParams | https://platform.openai.com/docs/api-reference/chat/create
|
|
23
|
+
class OpenAIChatCompletionParams(TypedDict, OpenAICreateBaseParams):
|
|
24
|
+
# NOTE: only for typing
|
|
25
|
+
messages: Iterable[ChatCompletionMessageParam] # required
|
|
26
|
+
model: str | ChatModel # required
|
|
27
|
+
frequency_penalty: float | None
|
|
28
|
+
logit_bias: dict[str, int] | None
|
|
29
|
+
logprobs: bool | None
|
|
30
|
+
max_completion_tokens: int | None
|
|
31
|
+
max_tokens: int | None
|
|
32
|
+
n: int | None
|
|
33
|
+
presence_penalty: float | None
|
|
34
|
+
reasoning_effort: ReasoningEffort | None
|
|
35
|
+
response_format: ResponseFormat
|
|
36
|
+
seed: int | None
|
|
37
|
+
temperature: float | None
|
|
38
|
+
top_p: float | None
|
|
39
|
+
tools: list[ChatCompletionToolParam] | None
|
|
40
|
+
tool_choice: Literal["none", "auto", "required"] | None
|
|
41
|
+
parallel_tool_calls: bool | None
|
|
42
|
+
stop: str | list[str] | None
|
|
43
|
+
top_logprobs: int | None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ResponseCreateParams | https://platform.openai.com/docs/api-reference/responses/create
|
|
47
|
+
class OpenAIResponsesParams(TypedDict, OpenAICreateBaseParams):
|
|
48
|
+
input: str | ResponseInputParam
|
|
49
|
+
instructions: str | None
|
|
50
|
+
max_output_tokens: int | None
|
|
51
|
+
max_tool_calls: int | None
|
|
52
|
+
model: ResponsesModel
|
|
53
|
+
parallel_tool_calls: bool | None
|
|
54
|
+
previous_response_id: str | None
|
|
55
|
+
reasoning: Reasoning | None
|
|
56
|
+
temperature: float | None
|
|
57
|
+
text: ResponseTextConfigParam
|
|
58
|
+
tool_choice: ToolChoice
|
|
59
|
+
tools: Iterable[ToolParam]
|
|
60
|
+
top_logprobs: int | None
|
|
61
|
+
top_p: float | None
|
|
62
|
+
truncation: Literal["auto", "disabled"] | None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
OpenAIChatCompletionParamsKeys = OpenAIChatCompletionParams.__annotations__.keys()
|
|
66
|
+
OpenAIResponsesParamsKeys = OpenAIResponsesParams.__annotations__.keys()
|
utils/path.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import pathlib
|
|
5
|
+
import tempfile
|
|
6
|
+
from typing import Any
|
|
7
|
+
from urllib.parse import urlparse
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
import yaml
|
|
11
|
+
from jinja2 import Environment, FileSystemLoader, Template
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_package_path() -> pathlib.Path:
|
|
15
|
+
return pathlib.Path(__file__).parent.parent.parent
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
DIR_ROOT = get_package_path()
|
|
19
|
+
CACHE_DIR = DIR_ROOT / ".cache"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FileUtils:
|
|
23
|
+
@staticmethod
|
|
24
|
+
def is_web_url(url: str) -> bool:
|
|
25
|
+
parsed_url = urlparse(url)
|
|
26
|
+
return all([parsed_url.scheme, parsed_url.netloc])
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def get_file_ext(file_path: str) -> str:
|
|
30
|
+
if FileUtils.is_web_url(file_path):
|
|
31
|
+
return pathlib.Path(urlparse(file_path).path).suffix
|
|
32
|
+
return pathlib.Path(file_path).suffix
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def download_file(url: str, save_path: str = None) -> str:
|
|
36
|
+
"""Download file from web. Return the saved path"""
|
|
37
|
+
# if not save_path, use tempfile
|
|
38
|
+
if not save_path:
|
|
39
|
+
save_path = tempfile.NamedTemporaryFile(
|
|
40
|
+
suffix=FileUtils.get_file_ext(url),
|
|
41
|
+
delete=False,
|
|
42
|
+
).name
|
|
43
|
+
response = requests.get(url)
|
|
44
|
+
response.raise_for_status()
|
|
45
|
+
with open(save_path, "wb") as f:
|
|
46
|
+
f.write(response.content)
|
|
47
|
+
return save_path
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def get_file_md5(file_path: str) -> str:
|
|
51
|
+
"""Clac md5 for local or web file"""
|
|
52
|
+
hash_md5 = hashlib.md5()
|
|
53
|
+
if FileUtils.is_web_url(file_path):
|
|
54
|
+
with requests.get(file_path, stream=True) as r:
|
|
55
|
+
r.raise_for_status()
|
|
56
|
+
for chunk in r.iter_content(chunk_size=4096):
|
|
57
|
+
hash_md5.update(chunk)
|
|
58
|
+
else:
|
|
59
|
+
with open(file_path, "rb") as f:
|
|
60
|
+
for chunk in iter(lambda: f.read(4096), b""):
|
|
61
|
+
hash_md5.update(chunk)
|
|
62
|
+
return hash_md5.hexdigest()
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def load_yaml(file_path: pathlib.Path | str) -> dict[str, Any]:
|
|
66
|
+
if isinstance(file_path, str):
|
|
67
|
+
file_path = pathlib.Path(file_path)
|
|
68
|
+
if not file_path.exists():
|
|
69
|
+
raise FileNotFoundError(f"File {file_path} does not exist")
|
|
70
|
+
with file_path.open() as f:
|
|
71
|
+
return yaml.safe_load(f)
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def load_prompts(fn: str | pathlib.Path) -> dict[str, str]:
|
|
75
|
+
"""Load prompts from yaml file.
|
|
76
|
+
|
|
77
|
+
- Default path: `DIR_ROOT / "utu/prompts" / fn`
|
|
78
|
+
"""
|
|
79
|
+
if isinstance(fn, str):
|
|
80
|
+
if not fn.endswith(".yaml"):
|
|
81
|
+
fn += ".yaml"
|
|
82
|
+
fn = DIR_ROOT / "utu" / "prompts" / fn
|
|
83
|
+
assert fn.exists(), f"File {fn} does not exist!"
|
|
84
|
+
with fn.open(encoding="utf-8") as f:
|
|
85
|
+
return yaml.safe_load(f)
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def get_jinja_env(directory: str | pathlib.Path) -> Environment:
|
|
89
|
+
if isinstance(directory, str):
|
|
90
|
+
directory = DIR_ROOT / "utu" / "prompts" / directory
|
|
91
|
+
if not directory.exists():
|
|
92
|
+
raise FileNotFoundError(f"Directory {directory} does not exist")
|
|
93
|
+
return Environment(loader=FileSystemLoader(directory))
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def get_jinja_template(template_path: str | pathlib.Path) -> Template:
|
|
97
|
+
if isinstance(template_path, str):
|
|
98
|
+
if not template_path.endswith(".j2"):
|
|
99
|
+
template_path += ".j2"
|
|
100
|
+
template_path = DIR_ROOT / "utu" / "prompts" / template_path
|
|
101
|
+
if not template_path.exists():
|
|
102
|
+
raise FileNotFoundError(f"File {template_path} does not exist")
|
|
103
|
+
with template_path.open(encoding="utf-8") as f:
|
|
104
|
+
return Template(f.read())
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def get_jinja_template_str(template_str: str) -> Template:
|
|
108
|
+
return Template(template_str)
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def load_json(file_path: str | pathlib.Path) -> dict[str, Any]:
|
|
112
|
+
if isinstance(file_path, str):
|
|
113
|
+
file_path = pathlib.Path(file_path)
|
|
114
|
+
if not file_path.exists():
|
|
115
|
+
raise FileNotFoundError(f"File {file_path} does not exist")
|
|
116
|
+
with file_path.open() as f:
|
|
117
|
+
return json.load(f)
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def load_json_data(file_path: str | pathlib.Path) -> list[dict[str, Any]]:
|
|
121
|
+
if isinstance(file_path, str) and not os.path.exists(file_path):
|
|
122
|
+
file_path = DIR_ROOT / "utu" / "data" / file_path
|
|
123
|
+
return FileUtils.load_json(file_path)
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def save_json(file_path: str | pathlib.Path, data: dict[str, Any]) -> None:
|
|
127
|
+
if isinstance(file_path, str):
|
|
128
|
+
file_path = pathlib.Path(file_path)
|
|
129
|
+
with file_path.open("w", encoding="utf-8") as f:
|
|
130
|
+
json.dump(data, f, ensure_ascii=False, indent=4)
|
utils/print_utils.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
|
|
4
|
+
import prompt_toolkit
|
|
5
|
+
from colorama import Fore, Style, init
|
|
6
|
+
from prompt_toolkit.patch_stdout import patch_stdout
|
|
7
|
+
|
|
8
|
+
init(autoreset=True) # Reset color to default (autoreset=True handles this automatically)
|
|
9
|
+
|
|
10
|
+
COLOR_DICT = defaultdict(lambda: Style.RESET_ALL)
|
|
11
|
+
COLOR_DICT.update(
|
|
12
|
+
{
|
|
13
|
+
"gray": Fore.LIGHTBLACK_EX,
|
|
14
|
+
"orange": Fore.LIGHTYELLOW_EX,
|
|
15
|
+
"red": Fore.RED,
|
|
16
|
+
"green": Fore.GREEN,
|
|
17
|
+
"blue": Fore.BLUE,
|
|
18
|
+
"yellow": Fore.YELLOW,
|
|
19
|
+
"magenta": Fore.MAGENTA,
|
|
20
|
+
"cyan": Fore.CYAN,
|
|
21
|
+
"white": Fore.WHITE,
|
|
22
|
+
"bold_blue": Style.BRIGHT + Fore.BLUE,
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PrintUtils:
|
|
28
|
+
@staticmethod
|
|
29
|
+
def print_input(prompt_text: str):
|
|
30
|
+
"""styled user input"""
|
|
31
|
+
# https://github.com/prompt-toolkit/python-prompt-toolkit
|
|
32
|
+
# user_input = input(COLOR_DICT[prompt_color] + prompt_text + COLOR_DICT[input_color])
|
|
33
|
+
# print(Style.RESET_ALL, end="")
|
|
34
|
+
user_input = prompt_toolkit.prompt(prompt_text)
|
|
35
|
+
return user_input
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
async def async_print_input(prompt_text: str) -> str:
|
|
39
|
+
# https://python-prompt-toolkit.readthedocs.io/en/master/pages/asking_for_input.html#prompt-in-an-asyncio-application # noqa: E501
|
|
40
|
+
session = prompt_toolkit.PromptSession()
|
|
41
|
+
while True:
|
|
42
|
+
with patch_stdout():
|
|
43
|
+
result = await session.prompt_async(prompt_text)
|
|
44
|
+
return result
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def print_info(
|
|
48
|
+
msg: str,
|
|
49
|
+
color: str = "gray",
|
|
50
|
+
add_prefix: bool = False,
|
|
51
|
+
prefix: str = "",
|
|
52
|
+
end: str = "\n",
|
|
53
|
+
flush: bool = True,
|
|
54
|
+
):
|
|
55
|
+
if add_prefix:
|
|
56
|
+
msg = prefix + " " + msg
|
|
57
|
+
print(COLOR_DICT[color] + msg + Style.RESET_ALL, end=end, flush=flush)
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def print_bot(
|
|
61
|
+
msg: str,
|
|
62
|
+
color: str = "orange",
|
|
63
|
+
add_prefix: bool = False,
|
|
64
|
+
prefix: str = "[BOT]",
|
|
65
|
+
end: str = "\n",
|
|
66
|
+
flush: bool = True,
|
|
67
|
+
):
|
|
68
|
+
PrintUtils.print_info(msg, color=color, add_prefix=add_prefix, prefix=prefix, end=end, flush=flush)
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def print_tool(
|
|
72
|
+
msg: str,
|
|
73
|
+
color: str = "green",
|
|
74
|
+
add_prefix: bool = False,
|
|
75
|
+
prefix: str = "[TOOL]",
|
|
76
|
+
end: str = "\n",
|
|
77
|
+
flush: bool = True,
|
|
78
|
+
):
|
|
79
|
+
PrintUtils.print_info(msg, color=color, add_prefix=add_prefix, prefix=prefix, end=end, flush=flush)
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def print_error(
|
|
83
|
+
msg: str,
|
|
84
|
+
color: str = "red",
|
|
85
|
+
add_prefix: bool = False,
|
|
86
|
+
prefix: str = "",
|
|
87
|
+
end: str = "\n",
|
|
88
|
+
flush: bool = True,
|
|
89
|
+
):
|
|
90
|
+
PrintUtils.print_info(msg, color=color, add_prefix=add_prefix, prefix=prefix, end=end, flush=flush)
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def format_json(obj: dict, indent: int = None) -> str:
|
|
94
|
+
return json.dumps(obj, indent=indent, ensure_ascii=False)
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def truncate_text(text: str | dict, max_length: int = 100, oneline: bool = True) -> str:
|
|
98
|
+
"""Truncate text to max_length, adding ellipsis if truncated.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
text: The text to truncate
|
|
102
|
+
max_length: Maximum length of the returned text
|
|
103
|
+
oneline: Whether to convert text to a single line by removing newlines
|
|
104
|
+
"""
|
|
105
|
+
if isinstance(text, dict):
|
|
106
|
+
text = PrintUtils.format_json(text)
|
|
107
|
+
if oneline:
|
|
108
|
+
text = " ".join(text.splitlines())
|
|
109
|
+
if len(text) <= max_length:
|
|
110
|
+
return text
|
|
111
|
+
else:
|
|
112
|
+
return text[: max_length - 3] + "..."
|
utils/script_utils.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from ..config import AgentConfig, ConfigLoader
|
|
4
|
+
from .print_utils import PrintUtils
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def parse_cli_args() -> AgentConfig:
|
|
8
|
+
parser = argparse.ArgumentParser()
|
|
9
|
+
parser.add_argument("--config_name", type=str, default="simple/base", help="Configuration name")
|
|
10
|
+
parser.add_argument("--agent_model", type=str, default=None, help="Agent model.")
|
|
11
|
+
args = parser.parse_args()
|
|
12
|
+
|
|
13
|
+
config: AgentConfig = ConfigLoader.load_agent_config(args.config_name)
|
|
14
|
+
# Override basic configs
|
|
15
|
+
if args.agent_model:
|
|
16
|
+
assert config.type == "simple", f"--agent_model only support SimpleAgent, get {config.type}"
|
|
17
|
+
config.model.model_provider.model = args.agent_model
|
|
18
|
+
if config.type == "workforce":
|
|
19
|
+
PrintUtils.print_info("Error: Workforce agent is not supported in CLI mode yet.")
|
|
20
|
+
raise NotImplementedError
|
|
21
|
+
|
|
22
|
+
return config
|