cota 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. cota/__init__.py +1 -0
  2. cota/__main__.py +284 -0
  3. cota/actions/__init__.py +6 -0
  4. cota/actions/action.py +93 -0
  5. cota/actions/bot_utter.py +118 -0
  6. cota/actions/constant.py +0 -0
  7. cota/actions/executors/base.py +63 -0
  8. cota/actions/executors/http.py +44 -0
  9. cota/actions/executors/plugin.py +109 -0
  10. cota/actions/executors/python.py +55 -0
  11. cota/actions/executors/script.py +62 -0
  12. cota/actions/form.py +235 -0
  13. cota/actions/plugins/base.py +35 -0
  14. cota/actions/plugins/example.py +36 -0
  15. cota/actions/rag.py +57 -0
  16. cota/actions/selector.py +102 -0
  17. cota/actions/user_utter.py +88 -0
  18. cota/agent.py +401 -0
  19. cota/channels/__init__.py +6 -0
  20. cota/channels/channel.py +141 -0
  21. cota/channels/cmdline.py +73 -0
  22. cota/channels/connection.py +120 -0
  23. cota/channels/room.py +161 -0
  24. cota/channels/socketio.py +272 -0
  25. cota/channels/sse.py +316 -0
  26. cota/channels/utils.py +48 -0
  27. cota/channels/websocket.py +277 -0
  28. cota/constant.py +174 -0
  29. cota/dpl/__init__.py +0 -0
  30. cota/dpl/dpl.py +137 -0
  31. cota/dpl/llm.py +307 -0
  32. cota/dpl/match.py +132 -0
  33. cota/dpl/trigger.py +175 -0
  34. cota/dst.py +464 -0
  35. cota/http_client.py +48 -0
  36. cota/knowledge/__init__.py +5 -0
  37. cota/knowledge/knowledge.py +131 -0
  38. cota/knowledge/llm_knowledge.py +127 -0
  39. cota/llm/__init__.py +20 -0
  40. cota/llm/base.py +36 -0
  41. cota/llm/custom.py +88 -0
  42. cota/llm/factory.py +61 -0
  43. cota/llm/llm.py +51 -0
  44. cota/llm/openai.py +139 -0
  45. cota/message/message.py +53 -0
  46. cota/processor.py +155 -0
  47. cota/server.py +99 -0
  48. cota/store.py +277 -0
  49. cota/task.py +216 -0
  50. cota/utils/__init__.py +0 -0
  51. cota/utils/common.py +115 -0
  52. cota/utils/http/__init__.py +4 -0
  53. cota/utils/http/client.py +153 -0
  54. cota/utils/http/manager.py +54 -0
  55. cota/utils/io.py +14 -0
  56. cota/utils/parser.py +67 -0
  57. cota-1.0.1.dist-info/METADATA +205 -0
  58. cota-1.0.1.dist-info/RECORD +61 -0
  59. cota-1.0.1.dist-info/WHEEL +4 -0
  60. cota-1.0.1.dist-info/entry_points.txt +3 -0
  61. cota-1.0.1.dist-info/licenses/LICENSE +21 -0
cota/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "1.0.1" # same as pyproject.toml
cota/__main__.py ADDED
@@ -0,0 +1,284 @@
1
+ # encoding:utf-8
2
+ import argparse
3
+ import os
4
+ import shutil
5
+ import sys
6
+ import logging
7
+ import ssl
8
+ from typing import Text, Optional, Dict, Any
9
+ from sanic import Sanic
10
+ import asyncio
11
+ from cota import server
12
+ from cota.task import Task
13
+ from cota.agent import Agent
14
+ from cota.channels import channel, socketio, websocket, cmdline, sse
15
+ from cota.utils.common import list_routes
16
+ from cota import __version__
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ def validate_ssl_files(cert_path: str, key_path: str) -> bool:
21
+ """Validate SSL certificate and key files"""
22
+ try:
23
+ if not os.path.exists(cert_path):
24
+ raise FileNotFoundError(f"SSL certificate file not found: {cert_path}")
25
+ if not os.path.exists(key_path):
26
+ raise FileNotFoundError(f"SSL key file not found: {key_path}")
27
+
28
+ context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
29
+ context.load_cert_chain(cert_path, keyfile=key_path)
30
+ return True
31
+ except (FileNotFoundError, ssl.SSLError) as e:
32
+ raise ValueError(f"Invalid SSL configuration: {str(e)}")
33
+
34
+ def create_server_config(args):
35
+ """Create server configuration including SSL settings"""
36
+ server_config = {
37
+ 'host': args.host,
38
+ 'port': args.port,
39
+ 'debug': args.debug
40
+ }
41
+
42
+ if args.ssl_cert and args.ssl_key:
43
+ # Validate SSL files before creating config
44
+ if validate_ssl_files(args.ssl_cert, args.ssl_key):
45
+ server_config['ssl'] = {
46
+ 'cert': args.ssl_cert,
47
+ 'key': args.ssl_key
48
+ }
49
+
50
+ return server_config
51
+
52
+ def run(args):
53
+ agent = Agent.load_from_path(path=args.config)
54
+ app = server.create_app(agent)
55
+
56
+ # Define channel configurations
57
+ channel_configs = {
58
+ 'socket.io': {
59
+ 'class': socketio.SocketIO,
60
+ 'kwargs': {},
61
+ 'register_kwargs': {'route': '/webhooks/'}
62
+ },
63
+ 'websocket': {
64
+ 'class': websocket.Websocket,
65
+ 'kwargs': {
66
+ 'connection_timeout': 1000,
67
+ 'room_timeout': 3600
68
+ },
69
+ 'register_kwargs': {}
70
+ },
71
+ 'sse': {
72
+ 'class': sse.SSE,
73
+ 'kwargs': {
74
+ 'connection_timeout': 1000,
75
+ 'room_timeout': 3600
76
+ },
77
+ 'register_kwargs': {}
78
+ }
79
+ }
80
+
81
+ # Initialize and register channel
82
+ if args.channel not in channel_configs:
83
+ raise ValueError(f"Unsupported channel: {args.channel}. "
84
+ f"Available channels: {list(channel_configs.keys())}")
85
+
86
+ config = channel_configs[args.channel]
87
+ channel_class = config['class']
88
+
89
+ try:
90
+ input_channels = channel_class(**config['kwargs'])
91
+ channel.register([input_channels], app, **config['register_kwargs'])
92
+
93
+ # Configure logging for the channel
94
+ logger = logging.getLogger(f"cota.channels.{args.channel}")
95
+ logger.info(f"Initialized {args.channel} channel")
96
+
97
+ list_routes(app)
98
+
99
+ # Run server with configuration
100
+ server_config = create_server_config(args)
101
+ app.run(**server_config)
102
+
103
+ except Exception as e:
104
+ logger.error(f"Failed to initialize {args.channel} channel: {e}")
105
+ raise
106
+
107
+
108
+ async def shell(args):
109
+ agent = Agent.load_from_path(path=args.config)
110
+ print("Agent loaded. Type a message and press enter.")
111
+
112
+ async def handler(message, channel):
113
+ await agent.processor.handle_message(message, channel)
114
+
115
+ cmdline_channel = cmdline.Cmdline(on_new_message=handler)
116
+ await cmdline_channel.on_connect()
117
+
118
+ async def task(args):
119
+ task = Task.load_from_path(path=args.config)
120
+ print("Task loaded.")
121
+ await task.run()
122
+
123
+ def init(args):
124
+ # Define the directory and files to be created
125
+ project_dir = "cota_projects"
126
+ template_dir = "bots" # Replace with the actual directory name
127
+
128
+ # Create the project directory
129
+ if not os.path.exists(project_dir):
130
+ os.makedirs(project_dir)
131
+ print(f"Created directory: {project_dir}")
132
+ else:
133
+ print(f"Directory {project_dir} already exists.")
134
+
135
+ # Copy the contents of the template directory
136
+ src_dir = os.path.join(os.path.dirname(__file__), template_dir)
137
+ if os.path.exists(src_dir):
138
+ for item in os.listdir(src_dir):
139
+ src_item = os.path.join(src_dir, item)
140
+ dst_item = os.path.join(project_dir, item)
141
+ if os.path.isdir(src_item):
142
+ shutil.copytree(src_item, dst_item)
143
+ else:
144
+ shutil.copy2(src_item, dst_item)
145
+ else:
146
+ print(f"Source directory {src_dir} does not exist.")
147
+
148
+ def configure_logging(log_level=logging.INFO):
149
+ """Configure logging with the specified level"""
150
+ # Set the root logger level
151
+ root_logger = logging.getLogger()
152
+ root_logger.setLevel(log_level)
153
+
154
+ # Configure basic logging format
155
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
156
+
157
+ # Add console handler if not already present
158
+ if not root_logger.handlers:
159
+ console_handler = logging.StreamHandler()
160
+ console_handler.setFormatter(formatter)
161
+ root_logger.addHandler(console_handler)
162
+ else:
163
+ # Update existing handlers' formatters
164
+ for handler in root_logger.handlers:
165
+ handler.setFormatter(formatter)
166
+
167
+ # Set level for all handlers
168
+ for handler in root_logger.handlers:
169
+ handler.setLevel(log_level)
170
+
171
+ def create_argument_parser() -> argparse.ArgumentParser:
172
+ """ """
173
+ # create the top-level parser
174
+ parser = argparse.ArgumentParser(
175
+ prog="cota",
176
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
177
+ description="cota command line interface."
178
+ )
179
+
180
+ parser.add_argument(
181
+ "--version",
182
+ action="store_true",
183
+ default=argparse.SUPPRESS,
184
+ help="Print installed cota version"
185
+ )
186
+
187
+ parent_parser = argparse.ArgumentParser(add_help=False)
188
+ parent_parsers = [parent_parser]
189
+
190
+ subparsers = parser.add_subparsers(help="cota commands")
191
+
192
+ # create the parser for the "run" command
193
+ parser_run = subparsers.add_parser(
194
+ "run",
195
+ parents=parent_parsers,
196
+ conflict_handler="resolve",
197
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
198
+ help="Starts a cota server."
199
+ )
200
+
201
+ parser_run.add_argument('--host', default='0.0.0.0', type=str, help="Host to run the service")
202
+ parser_run.add_argument('--port', default=5005, type=int, help="Port to run the service")
203
+ parser_run.add_argument('--config', default='./', type=str, help="agent config")
204
+ parser_run.add_argument('--channel', default='socket.io', type=str, help="Message channel")
205
+ parser_run.add_argument('--debug', action='store_true', help="Enable debug mode (sets log level to DEBUG)")
206
+ parser_run.add_argument('--log', default='INFO', type=str, help="Set the logging level")
207
+ parser_run.add_argument('--ssl-cert', type=str, help="SSL certificate file path")
208
+ parser_run.add_argument('--ssl-key', type=str, help="SSL private key file path")
209
+ def run_with_debug(args):
210
+ if args.debug:
211
+ args.log = 'DEBUG'
212
+ configure_logging(getattr(logging, args.log.upper(), logging.INFO))
213
+ run(args)
214
+ parser_run.set_defaults(func=run_with_debug)
215
+
216
+ # create the parser for the "shell" command
217
+ parser_shell = subparsers.add_parser(
218
+ "shell",
219
+ parents=parent_parsers,
220
+ conflict_handler="resolve",
221
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
222
+ help="Interacting with cota by commandline."
223
+ )
224
+
225
+ parser_shell.add_argument('--config', default='./', type=str, help="agent config path")
226
+ parser_shell.add_argument('--log', default='INFO', type=str, help="Set the logging level")
227
+ parser_shell.add_argument('--debug', action='store_true', help="Enable debug mode (sets log level to DEBUG)")
228
+
229
+ def run_shell(args):
230
+ if args.debug:
231
+ args.log = 'DEBUG'
232
+ configure_logging(getattr(logging, args.log.upper(), logging.INFO))
233
+ return asyncio.run(shell(args))
234
+
235
+ parser_shell.set_defaults(func=run_shell)
236
+
237
+ # create the parser for the "task" command
238
+ parser_task = subparsers.add_parser(
239
+ "task",
240
+ parents=parent_parsers,
241
+ conflict_handler="resolve",
242
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
243
+ help="Executing cota in task mode."
244
+ )
245
+
246
+ parser_task.add_argument('--config', default='./', type=str, help="task config path")
247
+ parser_task.add_argument('--log', default='INFO', type=str, help="Set the logging level")
248
+ parser_task.add_argument('--debug', action='store_true', help="Enable debug mode (sets log level to DEBUG)")
249
+
250
+ def run_task(args):
251
+ if args.debug:
252
+ args.log = 'DEBUG'
253
+ configure_logging(getattr(logging, args.log.upper(), logging.INFO))
254
+ return asyncio.run(task(args))
255
+
256
+ parser_task.set_defaults(func=run_task)
257
+
258
+ # create the parser for the "init" command
259
+ parser_init = subparsers.add_parser(
260
+ "init",
261
+ parents=parent_parsers,
262
+ conflict_handler="resolve",
263
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
264
+ help="Initialize a new cota project."
265
+ )
266
+ parser_init.add_argument('--log', default='INFO', type=str, help="Set the logging level")
267
+ parser_init.set_defaults(func=init)
268
+
269
+ return parser
270
+
271
+ def main():
272
+ parser = create_argument_parser()
273
+ cmdline_arguments = parser.parse_args()
274
+
275
+ if hasattr(cmdline_arguments, "func"):
276
+ log_level = getattr(logging, cmdline_arguments.log.upper())
277
+ configure_logging(log_level)
278
+ cmdline_arguments.func(cmdline_arguments)
279
+ elif hasattr(cmdline_arguments, "version"):
280
+ print(f"Cota version: {__version__}")
281
+
282
+
283
+ if __name__ == "__main__":
284
+ main()
@@ -0,0 +1,6 @@
1
+ from cota.actions.action import Action
2
+ from cota.actions.user_utter import UserUtter
3
+ from cota.actions.bot_utter import BotUtter
4
+ from cota.actions.selector import Selector
5
+ from cota.actions.form import Form
6
+ from cota.actions.rag import RAG
cota/actions/action.py ADDED
@@ -0,0 +1,93 @@
1
+ import time
2
+ from typing import Text, Optional, Any, Dict, List
3
+
4
+ registry = []
5
+
6
+
7
+ class Action:
8
+ def __init__(
9
+ self,
10
+ name: Optional[Text] = None,
11
+ description: Optional[Text] = None,
12
+ prompt: Optional[Text] = None,
13
+ llm: Optional[Text] = None,
14
+ timestamp: Optional[float] = None,
15
+ sender_id: Optional[Text] = None,
16
+ receiver_id: Optional[Text] = None,
17
+ result: Optional[List] = None
18
+ ) -> None:
19
+ self.name = name
20
+ self.description = description
21
+ self.prompt = prompt
22
+ self.llm = llm
23
+ self.timestamp = timestamp or time.time()
24
+ self.sender_id = sender_id or 'default_sender'
25
+ self.receiver_id = receiver_id or 'default_receiver'
26
+ self.result = result or list()
27
+
28
+ def __init_subclass__(cls, **kwargs):
29
+ super().__init_subclass__(**kwargs)
30
+ registry.append(cls)
31
+
32
+ @staticmethod
33
+ def resolve_by_type(
34
+ name: Text
35
+ ) -> "Action":
36
+ # Try exact match first
37
+ for cls in registry:
38
+ if cls.__name__ == name:
39
+ return cls
40
+
41
+ # Try case-insensitive match
42
+ name_lower = name.lower()
43
+ for cls in registry:
44
+ if cls.__name__.lower() == name_lower:
45
+ return cls
46
+
47
+ # if not in register
48
+ new_class = type(name, (Action,), {})
49
+ new_class.__name__ = name
50
+ return new_class
51
+
52
+
53
+ def apply_to(self,dst) -> None:
54
+ dst.actions.append(self)
55
+ dst.latest_action = self
56
+
57
+ @staticmethod
58
+ def build_from_name(**kwargs) -> "Action":
59
+ action_class = Action.resolve_by_type(kwargs.get("name"))
60
+ if not action_class:
61
+ return None
62
+ return action_class(**kwargs)
63
+
64
+ def run_from_dict(
65
+ self,
66
+ action_dict
67
+ ) -> None:
68
+ self.name = action_dict.get("name") or self.name
69
+ self.timestamp = action_dict.get("timestamp") or self.timestamp
70
+ self.sender_id = action_dict.get("sender_id") or self.sender_id
71
+ self.receiver_id = action_dict.get("receiver_id") or self.receiver_id
72
+ self.result = action_dict.get("result")
73
+
74
+ def run_from_string(
75
+ self,
76
+ text
77
+ ) -> None:
78
+ """Implementation based on string"""
79
+ raise NotImplementedError
80
+
81
+ async def run(self, agent, dst) -> None:
82
+ raise NotImplementedError
83
+
84
+ def as_dict(self) -> Dict[Text, Any]:
85
+ d = {
86
+ "name": self.__class__.__name__,
87
+ "timestamp": self.timestamp,
88
+ "sender_id": self.sender_id,
89
+ "receiver_id": self.receiver_id
90
+ }
91
+ if self.result:
92
+ d["result"] = self.result
93
+ return d
@@ -0,0 +1,118 @@
1
+ import logging
2
+ import json
3
+ from typing import Optional, Dict, Any, List
4
+ from cota.actions.action import Action
5
+ from cota.message.message import Message
6
+ from cota.dst import DST
7
+ from cota.constant import DEFAULT_DIALOGUE_MAX_TOKENS
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class BotUtter(Action):
13
+ """
14
+ Bot utterance action that generates responses using LLM with JSON output support.
15
+
16
+ This action processes prompts through the dialogue state tracker (DST),
17
+ formats them with RAG and thought context, and generates bot responses
18
+ using the configured language model. The LLM is instructed to return
19
+ responses in JSON format with 'text' and 'thought' fields.
20
+
21
+ Expected JSON response format:
22
+ {
23
+ "text": "The main response content",
24
+ "thought": "Internal reasoning or thought process"
25
+ }
26
+
27
+ The 'thought' content is stored at the same level as 'text' for analysis and debugging.
28
+ """
29
+ def apply_to(self, dst: DST) -> None:
30
+ """
31
+ Apply this action to the dialogue state tracker.
32
+
33
+ Args:
34
+ dst: The dialogue state tracker to update
35
+ """
36
+ if dst is None:
37
+ raise ValueError("DST cannot be None")
38
+
39
+ dst.actions.append(self)
40
+ dst.formless_actions.append(self)
41
+ dst.latest_action = self
42
+ dst.latest_response = self
43
+
44
+ def run_from_string(self, text: str) -> None:
45
+ """
46
+ Create a bot message from a string and add it to results.
47
+
48
+ Args:
49
+ text: The text content for the bot message
50
+ """
51
+ if not text:
52
+ logger.warning("Empty text provided to run_from_string")
53
+ return
54
+
55
+ if not hasattr(self, 'result'):
56
+ self.result = []
57
+
58
+ message = Message(sender='bot', text=text)
59
+ self.result.append(message.as_dict())
60
+
61
+ async def run(
62
+ self,
63
+ agent: Optional["Agent"] = None,
64
+ dst: Optional[DST] = None,
65
+ ):
66
+ knowledge_dict = await dst.format_knowledge(self.prompt, self)
67
+ thoughts_dict = await dst.format_policies(self.prompt, self)
68
+ prompt = dst.format_prompt(self.prompt, self, {**knowledge_dict, **thoughts_dict})
69
+
70
+ messages = [
71
+ {
72
+ "role": "system",
73
+ "content": dst.agent_description()
74
+ },
75
+ {
76
+ "role": "user",
77
+ "content": prompt
78
+ }
79
+ ]
80
+
81
+ # Generate response with JSON format requirement
82
+ result = await agent.llm_instance(self.llm).generate_chat(
83
+ messages=messages,
84
+ max_tokens=agent.dialogue.get('max_tokens', DEFAULT_DIALOGUE_MAX_TOKENS),
85
+ response_format={'type': 'json_object'}
86
+ )
87
+
88
+ # Parse JSON response from content field
89
+ content = result["content"]
90
+ try:
91
+ json_result = json.loads(content)
92
+ text_content = json_result.get('text', '')
93
+ thought_content = json_result.get('thought', '')
94
+
95
+ logger.debug(f"Parsed JSON result: {json_result}")
96
+ except json.JSONDecodeError:
97
+ logger.error(f"Failed to parse JSON response from bot: {content}")
98
+ # Fallback to original text if JSON parsing fails
99
+ text_content = content
100
+ thought_content = ''
101
+
102
+ # Create message with text content
103
+ message = Message(sender='bot', sender_id=agent.name, text=text_content)
104
+
105
+ # Create message dict and add thought at the same level as text
106
+ message_dict = message.as_dict()
107
+ if thought_content:
108
+ message_dict['thought'] = thought_content
109
+
110
+ if self.result and self.result[-1].get('text') is None:
111
+ self.result[-1].update({
112
+ key: value for key, value in message_dict.items() if value is not None
113
+ })
114
+ else:
115
+ self.result.append(message_dict)
116
+
117
+ logger.debug(f"Response prompt: {prompt}")
118
+ logger.debug(f"Response result: {self.result}")
File without changes
@@ -0,0 +1,63 @@
1
+ from typing import Dict, Text, Tuple, Any
2
+ from abc import ABC, abstractmethod
3
+
4
+ class Executor(ABC):
5
+ """Base executor class
6
+
7
+ Defines the basic interface for executors, all concrete executors should inherit from this class.
8
+ """
9
+
10
+ def __init__(self, config: Dict[Text, Any]) -> None:
11
+ self.config = config
12
+
13
+ @abstractmethod
14
+ async def execute(self, data: Dict[Text, Any]) -> Tuple[Text, Dict]:
15
+ """Execute specific operation
16
+
17
+ Args:
18
+ data: Data required for execution
19
+
20
+ Returns:
21
+ Tuple[Response text, Metadata]
22
+ """
23
+ pass
24
+
25
+ async def cleanup(self) -> None:
26
+ """Clean up resources
27
+
28
+ Subclasses can override this method to implement resource cleanup logic.
29
+ """
30
+ pass
31
+
32
+ @classmethod
33
+ def create(cls, executor_type: Text, config: Dict[Text, Any]) -> "Executor":
34
+ """Create executor instance
35
+
36
+ Args:
37
+ executor_type: Executor type
38
+ config: Executor configuration
39
+
40
+ Returns:
41
+ Executor: Executor instance
42
+ """
43
+ from .http import HttpExecutor
44
+ from .script import ScriptExecutor
45
+ from .python import PythonExecutor
46
+ from .plugin import PluginExecutor
47
+
48
+ executor_map = {
49
+ "http": HttpExecutor,
50
+ "script": ScriptExecutor,
51
+ "python": PythonExecutor,
52
+ "plugin": PluginExecutor
53
+ }
54
+
55
+ executor_class = executor_map.get(executor_type)
56
+ if not executor_class:
57
+ raise ValueError(f"Unknown executor type: {executor_type}")
58
+
59
+ # Remove type information as it has been used
60
+ config = config.copy()
61
+ config.pop("type", None)
62
+
63
+ return executor_class(config)
@@ -0,0 +1,44 @@
1
+ import logging
2
+ from typing import Dict, Text, Tuple, Any
3
+ from .base import Executor
4
+ from cota.utils.http import HttpClientManager
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ class HttpExecutor(Executor):
9
+ """HTTP executor
10
+
11
+ Handles execution logic based on HTTP requests.
12
+ """
13
+
14
+ async def execute(self, data: Dict[Text, Any]) -> Tuple[Text, Dict]:
15
+ """Execute HTTP request
16
+
17
+ Args:
18
+ data: Request data
19
+
20
+ Returns:
21
+ Tuple[Response text, Metadata]
22
+ """
23
+ url = self.config.get("url", "")
24
+ method = self.config.get("method", "get").upper()
25
+
26
+ try:
27
+ # Get HTTP client
28
+ client = await HttpClientManager.instance().get_client(
29
+ self.config.get("client_key", "default")
30
+ )
31
+
32
+ # Execute request
33
+ if method == "GET":
34
+ response = await client.get(url, params=data)
35
+ else:
36
+ response = await client.request(method, url, json_data=data)
37
+
38
+ if not response.ok:
39
+ raise Exception(f"Request failed with status {response.status}")
40
+
41
+ return response.data.get("text", ""), response.data.get("metadata", {})
42
+ except Exception as e:
43
+ logger.error(f"Failed to execute HTTP request: {str(e)}")
44
+ return "", {"error": str(e)}