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.
- cota/__init__.py +1 -0
- cota/__main__.py +284 -0
- cota/actions/__init__.py +6 -0
- cota/actions/action.py +93 -0
- cota/actions/bot_utter.py +118 -0
- cota/actions/constant.py +0 -0
- cota/actions/executors/base.py +63 -0
- cota/actions/executors/http.py +44 -0
- cota/actions/executors/plugin.py +109 -0
- cota/actions/executors/python.py +55 -0
- cota/actions/executors/script.py +62 -0
- cota/actions/form.py +235 -0
- cota/actions/plugins/base.py +35 -0
- cota/actions/plugins/example.py +36 -0
- cota/actions/rag.py +57 -0
- cota/actions/selector.py +102 -0
- cota/actions/user_utter.py +88 -0
- cota/agent.py +401 -0
- cota/channels/__init__.py +6 -0
- cota/channels/channel.py +141 -0
- cota/channels/cmdline.py +73 -0
- cota/channels/connection.py +120 -0
- cota/channels/room.py +161 -0
- cota/channels/socketio.py +272 -0
- cota/channels/sse.py +316 -0
- cota/channels/utils.py +48 -0
- cota/channels/websocket.py +277 -0
- cota/constant.py +174 -0
- cota/dpl/__init__.py +0 -0
- cota/dpl/dpl.py +137 -0
- cota/dpl/llm.py +307 -0
- cota/dpl/match.py +132 -0
- cota/dpl/trigger.py +175 -0
- cota/dst.py +464 -0
- cota/http_client.py +48 -0
- cota/knowledge/__init__.py +5 -0
- cota/knowledge/knowledge.py +131 -0
- cota/knowledge/llm_knowledge.py +127 -0
- cota/llm/__init__.py +20 -0
- cota/llm/base.py +36 -0
- cota/llm/custom.py +88 -0
- cota/llm/factory.py +61 -0
- cota/llm/llm.py +51 -0
- cota/llm/openai.py +139 -0
- cota/message/message.py +53 -0
- cota/processor.py +155 -0
- cota/server.py +99 -0
- cota/store.py +277 -0
- cota/task.py +216 -0
- cota/utils/__init__.py +0 -0
- cota/utils/common.py +115 -0
- cota/utils/http/__init__.py +4 -0
- cota/utils/http/client.py +153 -0
- cota/utils/http/manager.py +54 -0
- cota/utils/io.py +14 -0
- cota/utils/parser.py +67 -0
- cota-1.0.1.dist-info/METADATA +205 -0
- cota-1.0.1.dist-info/RECORD +61 -0
- cota-1.0.1.dist-info/WHEEL +4 -0
- cota-1.0.1.dist-info/entry_points.txt +3 -0
- 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()
|
cota/actions/__init__.py
ADDED
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}")
|
cota/actions/constant.py
ADDED
|
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)}
|