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.
@@ -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