webscout 8.2.5__py3-none-any.whl → 8.2.6__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.
Potentially problematic release.
This version of webscout might be problematic. Click here for more details.
- webscout/AIauto.py +112 -22
- webscout/AIutel.py +240 -344
- webscout/Extra/autocoder/autocoder.py +66 -5
- webscout/Provider/AISEARCH/scira_search.py +2 -1
- webscout/Provider/GizAI.py +6 -4
- webscout/Provider/Nemotron.py +218 -0
- webscout/Provider/OPENAI/scirachat.py +2 -1
- webscout/Provider/TeachAnything.py +8 -5
- webscout/Provider/WiseCat.py +1 -1
- webscout/Provider/WrDoChat.py +370 -0
- webscout/Provider/__init__.py +4 -6
- webscout/Provider/ai4chat.py +5 -3
- webscout/Provider/akashgpt.py +59 -66
- webscout/Provider/freeaichat.py +57 -43
- webscout/Provider/scira_chat.py +2 -1
- webscout/Provider/scnet.py +4 -1
- webscout/__init__.py +0 -1
- webscout/conversation.py +305 -446
- webscout/swiftcli/__init__.py +80 -794
- webscout/swiftcli/core/__init__.py +7 -0
- webscout/swiftcli/core/cli.py +297 -0
- webscout/swiftcli/core/context.py +104 -0
- webscout/swiftcli/core/group.py +241 -0
- webscout/swiftcli/decorators/__init__.py +28 -0
- webscout/swiftcli/decorators/command.py +221 -0
- webscout/swiftcli/decorators/options.py +220 -0
- webscout/swiftcli/decorators/output.py +252 -0
- webscout/swiftcli/exceptions.py +21 -0
- webscout/swiftcli/plugins/__init__.py +9 -0
- webscout/swiftcli/plugins/base.py +135 -0
- webscout/swiftcli/plugins/manager.py +262 -0
- webscout/swiftcli/utils/__init__.py +59 -0
- webscout/swiftcli/utils/formatting.py +252 -0
- webscout/swiftcli/utils/parsing.py +267 -0
- webscout/version.py +1 -1
- {webscout-8.2.5.dist-info → webscout-8.2.6.dist-info}/METADATA +1 -1
- {webscout-8.2.5.dist-info → webscout-8.2.6.dist-info}/RECORD +41 -28
- webscout/LLM.py +0 -442
- webscout/Provider/PizzaGPT.py +0 -228
- webscout/Provider/promptrefine.py +0 -193
- webscout/Provider/tutorai.py +0 -270
- {webscout-8.2.5.dist-info → webscout-8.2.6.dist-info}/WHEEL +0 -0
- {webscout-8.2.5.dist-info → webscout-8.2.6.dist-info}/entry_points.txt +0 -0
- {webscout-8.2.5.dist-info → webscout-8.2.6.dist-info}/licenses/LICENSE.md +0 -0
- {webscout-8.2.5.dist-info → webscout-8.2.6.dist-info}/top_level.txt +0 -0
webscout/conversation.py
CHANGED
|
@@ -2,59 +2,71 @@ import os
|
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
4
|
from typing import Optional, Dict, List, Any, TypedDict, Callable, TypeVar, Union
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
5
7
|
|
|
6
8
|
T = TypeVar('T')
|
|
7
9
|
|
|
10
|
+
class ConversationError(Exception):
|
|
11
|
+
"""Base exception for conversation-related errors."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
class ToolCallError(ConversationError):
|
|
15
|
+
"""Raised when there's an error with tool calls."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
class MessageValidationError(ConversationError):
|
|
19
|
+
"""Raised when message validation fails."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Message:
|
|
24
|
+
"""Represents a single message in the conversation."""
|
|
25
|
+
role: str
|
|
26
|
+
content: str
|
|
27
|
+
timestamp: datetime = datetime.now()
|
|
28
|
+
metadata: Dict[str, Any] = None
|
|
29
|
+
|
|
30
|
+
def __post_init__(self):
|
|
31
|
+
if self.metadata is None:
|
|
32
|
+
self.metadata = {}
|
|
8
33
|
|
|
9
34
|
class FunctionCall(TypedDict):
|
|
10
35
|
"""Type for a function call."""
|
|
11
36
|
name: str
|
|
12
37
|
arguments: Dict[str, Any]
|
|
13
38
|
|
|
14
|
-
|
|
15
39
|
class ToolDefinition(TypedDict):
|
|
16
40
|
"""Type for a tool definition."""
|
|
17
41
|
type: str
|
|
18
42
|
function: Dict[str, Any]
|
|
19
43
|
|
|
20
|
-
|
|
21
44
|
class FunctionCallData(TypedDict, total=False):
|
|
22
45
|
"""Type for function call data"""
|
|
23
46
|
tool_calls: List[FunctionCall]
|
|
24
47
|
error: str
|
|
25
48
|
|
|
26
|
-
|
|
27
49
|
class Fn:
|
|
28
|
-
"""
|
|
29
|
-
Represents a function (tool) that the agent can call.
|
|
30
|
-
"""
|
|
50
|
+
"""Represents a function (tool) that the agent can call."""
|
|
31
51
|
def __init__(self, name: str, description: str, parameters: Dict[str, str]) -> None:
|
|
32
52
|
self.name: str = name
|
|
33
53
|
self.description: str = description
|
|
34
54
|
self.parameters: Dict[str, str] = parameters
|
|
35
55
|
|
|
36
|
-
|
|
37
56
|
def tools(func: Callable[..., T]) -> Callable[..., T]:
|
|
38
|
-
"""Decorator to mark a function as a tool
|
|
57
|
+
"""Decorator to mark a function as a tool."""
|
|
39
58
|
func._is_tool = True # type: ignore
|
|
40
59
|
return func
|
|
41
60
|
|
|
42
|
-
|
|
43
61
|
class Conversation:
|
|
44
|
-
"""
|
|
45
|
-
|
|
46
|
-
This class is responsible for managing chat conversations, including:
|
|
47
|
-
- Maintaining chat history
|
|
48
|
-
- Loading/saving conversations from/to files
|
|
49
|
-
- Generating prompts based on context
|
|
50
|
-
- Managing token limits and history pruning
|
|
51
|
-
- Supporting tool calling functionality
|
|
62
|
+
"""Modern conversation manager with enhanced features.
|
|
52
63
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
Key Features:
|
|
65
|
+
- Robust message handling with metadata
|
|
66
|
+
- Enhanced tool calling support
|
|
67
|
+
- Efficient history management
|
|
68
|
+
- Improved error handling
|
|
69
|
+
- Memory optimization
|
|
58
70
|
"""
|
|
59
71
|
|
|
60
72
|
intro = (
|
|
@@ -69,130 +81,136 @@ class Conversation:
|
|
|
69
81
|
filepath: Optional[str] = None,
|
|
70
82
|
update_file: bool = True,
|
|
71
83
|
tools: Optional[List[Fn]] = None,
|
|
84
|
+
compression_threshold: int = 10000,
|
|
72
85
|
):
|
|
73
|
-
"""Initialize
|
|
74
|
-
|
|
75
|
-
Args:
|
|
76
|
-
status (bool): Flag to control history tracking. Defaults to True.
|
|
77
|
-
max_tokens (int): Maximum tokens for completion response. Defaults to 600.
|
|
78
|
-
filepath (str, optional): Path to save/load conversation history. Defaults to None.
|
|
79
|
-
update_file (bool): Whether to append new messages to file. Defaults to True.
|
|
80
|
-
tools (List[Fn], optional): List of tools available for the conversation. Defaults to None.
|
|
81
|
-
|
|
82
|
-
Examples:
|
|
83
|
-
>>> chat = Conversation(max_tokens=500)
|
|
84
|
-
>>> chat = Conversation(filepath="chat_history.txt")
|
|
85
|
-
"""
|
|
86
|
+
"""Initialize conversation manager with modern features."""
|
|
86
87
|
self.status = status
|
|
87
88
|
self.max_tokens_to_sample = max_tokens
|
|
88
|
-
self.
|
|
89
|
-
# Updated history formats
|
|
89
|
+
self.messages: List[Message] = []
|
|
90
90
|
self.history_format = "\nUser: %(user)s\nAssistant: %(llm)s"
|
|
91
|
-
# Tool format: Assistant outputs the tool call, then Tool provides the result
|
|
92
91
|
self.tool_history_format = "\nUser: %(user)s\nAssistant: <tool_call>%(tool_json)s</tool_call>\nTool: %(result)s"
|
|
93
92
|
self.file = filepath
|
|
94
93
|
self.update_file = update_file
|
|
95
94
|
self.history_offset = 10250
|
|
96
95
|
self.prompt_allowance = 10
|
|
97
96
|
self.tools = tools or []
|
|
97
|
+
self.compression_threshold = compression_threshold
|
|
98
|
+
self.logger = self._setup_logger()
|
|
98
99
|
|
|
99
100
|
if filepath:
|
|
100
101
|
self.load_conversation(filepath, False)
|
|
101
102
|
|
|
102
|
-
def
|
|
103
|
-
"""
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
def _setup_logger(self) -> logging.Logger:
|
|
104
|
+
"""Set up enhanced logging."""
|
|
105
|
+
logger = logging.getLogger("conversation")
|
|
106
|
+
if not logger.handlers:
|
|
107
|
+
handler = logging.StreamHandler()
|
|
108
|
+
formatter = logging.Formatter(
|
|
109
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
110
|
+
)
|
|
111
|
+
handler.setFormatter(formatter)
|
|
112
|
+
logger.addHandler(handler)
|
|
113
|
+
logger.setLevel(logging.INFO)
|
|
114
|
+
return logger
|
|
108
115
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
116
|
+
def load_conversation(self, filepath: str, exists: bool = True) -> None:
|
|
117
|
+
"""Load conversation with improved error handling."""
|
|
118
|
+
try:
|
|
119
|
+
if not isinstance(filepath, str):
|
|
120
|
+
raise TypeError(f"Filepath must be str, not {type(filepath)}")
|
|
121
|
+
|
|
122
|
+
if exists and not os.path.isfile(filepath):
|
|
123
|
+
raise FileNotFoundError(f"File '{filepath}' does not exist")
|
|
124
|
+
|
|
125
|
+
if not os.path.isfile(filepath):
|
|
126
|
+
with open(filepath, "w", encoding="utf-8") as fh:
|
|
127
|
+
fh.write(self.intro)
|
|
128
|
+
else:
|
|
129
|
+
with open(filepath, encoding="utf-8") as fh:
|
|
130
|
+
file_contents = fh.readlines()
|
|
131
|
+
if file_contents:
|
|
132
|
+
self.intro = file_contents[0]
|
|
133
|
+
self._process_history_from_file(file_contents[1:])
|
|
134
|
+
except Exception as e:
|
|
135
|
+
self.logger.error(f"Error loading conversation: {str(e)}")
|
|
136
|
+
raise ConversationError(f"Failed to load conversation: {str(e)}") from e
|
|
137
|
+
|
|
138
|
+
def _process_history_from_file(self, lines: List[str]) -> None:
|
|
139
|
+
"""Process and structure conversation history from file."""
|
|
140
|
+
current_role = None
|
|
141
|
+
current_content = []
|
|
142
|
+
|
|
143
|
+
for line in lines:
|
|
144
|
+
line = line.strip()
|
|
145
|
+
if line.startswith(("User:", "Assistant:", "Tool:")):
|
|
146
|
+
if current_role and current_content:
|
|
147
|
+
self.messages.append(Message(
|
|
148
|
+
role=current_role,
|
|
149
|
+
content="\n".join(current_content)
|
|
150
|
+
))
|
|
151
|
+
current_content = []
|
|
152
|
+
current_role = line.split(":")[0].lower()
|
|
153
|
+
content = ":".join(line.split(":")[1:]).strip()
|
|
154
|
+
current_content.append(content)
|
|
155
|
+
elif line:
|
|
156
|
+
current_content.append(line)
|
|
157
|
+
|
|
158
|
+
if current_role and current_content:
|
|
159
|
+
self.messages.append(Message(
|
|
160
|
+
role=current_role,
|
|
161
|
+
content="\n".join(current_content)
|
|
162
|
+
))
|
|
163
|
+
|
|
164
|
+
def _compress_history(self) -> None:
|
|
165
|
+
"""Compress history when it exceeds threshold."""
|
|
166
|
+
if len(self.messages) > self.compression_threshold:
|
|
167
|
+
# Keep recent messages and summarize older ones
|
|
168
|
+
keep_recent = 100 # Adjust based on needs
|
|
169
|
+
self.messages = (
|
|
170
|
+
[self._summarize_messages(self.messages[:-keep_recent])] +
|
|
171
|
+
self.messages[-keep_recent:]
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def _summarize_messages(self, messages: List[Message]) -> Message:
|
|
175
|
+
"""Create a summary message from older messages."""
|
|
176
|
+
return Message(
|
|
177
|
+
role="system",
|
|
178
|
+
content="[History Summary] Previous conversation summarized for context",
|
|
179
|
+
metadata={"summarized_count": len(messages)}
|
|
180
|
+
)
|
|
157
181
|
|
|
158
182
|
def gen_complete_prompt(self, prompt: str, intro: Optional[str] = None) -> str:
|
|
159
|
-
"""Generate
|
|
160
|
-
|
|
161
|
-
This method:
|
|
162
|
-
- Combines the intro, history, and new prompt
|
|
163
|
-
- Adds tools information if available
|
|
164
|
-
- Trims history if needed
|
|
165
|
-
- Keeps everything organized and flowing
|
|
166
|
-
|
|
167
|
-
Args:
|
|
168
|
-
prompt (str): Your message to add to the chat
|
|
169
|
-
intro (str, optional): Custom intro to use. Default: None (uses class intro)
|
|
170
|
-
|
|
171
|
-
Returns:
|
|
172
|
-
str: The complete conversation prompt, ready for the LLM!
|
|
173
|
-
|
|
174
|
-
Examples:
|
|
175
|
-
>>> chat = Conversation()
|
|
176
|
-
>>> prompt = chat.gen_complete_prompt("What's good?")
|
|
177
|
-
"""
|
|
183
|
+
"""Generate complete prompt with enhanced context management."""
|
|
178
184
|
if not self.status:
|
|
179
185
|
return prompt
|
|
180
186
|
|
|
181
|
-
intro = intro or self.intro
|
|
182
|
-
'''You are a helpful and versatile AI assistant. Your goal is to provide concise and informative responses directly to user queries. Use available tools in correct format to enhance responses or execute actions as needed.
|
|
183
|
-
''')
|
|
187
|
+
intro = intro or self.intro
|
|
184
188
|
|
|
185
|
-
# Add tool information if
|
|
189
|
+
# Add tool information if available
|
|
186
190
|
tools_description = self.get_tools_description()
|
|
187
191
|
if tools_description:
|
|
188
192
|
try:
|
|
189
|
-
from datetime import datetime
|
|
190
193
|
date_str = f"Current date: {datetime.now().strftime('%d %b %Y')}"
|
|
191
194
|
except:
|
|
192
195
|
date_str = ""
|
|
193
196
|
|
|
194
|
-
intro = (
|
|
195
|
-
|
|
197
|
+
intro = self._generate_enhanced_intro(intro, tools_description, date_str)
|
|
198
|
+
|
|
199
|
+
# Generate history string with proper formatting
|
|
200
|
+
history = self._generate_history_string()
|
|
201
|
+
|
|
202
|
+
# Combine and trim if needed
|
|
203
|
+
complete_prompt = intro + self._trim_chat_history(
|
|
204
|
+
history + "\nUser: " + prompt + "\nAssistant:",
|
|
205
|
+
intro
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return complete_prompt
|
|
209
|
+
|
|
210
|
+
def _generate_enhanced_intro(self, intro: str, tools_description: str, date_str: str) -> str:
|
|
211
|
+
"""Generate enhanced introduction with tools and guidelines."""
|
|
212
|
+
return f'''
|
|
213
|
+
{intro}
|
|
196
214
|
|
|
197
215
|
{date_str}
|
|
198
216
|
|
|
@@ -201,377 +219,218 @@ class Conversation:
|
|
|
201
219
|
Your goal is to assist the user effectively. Analyze each query and choose one of two response modes:
|
|
202
220
|
|
|
203
221
|
**1. Tool Mode:**
|
|
204
|
-
- **When:** If the query requires external data, calculations, or functions listed under AVAILABLE TOOLS
|
|
205
|
-
- **Action:** Output *ONLY* the complete JSON tool call
|
|
206
|
-
- **
|
|
207
|
-
- **Example (Output is *only* this block):**
|
|
208
|
-
```json
|
|
209
|
-
<tool_call>
|
|
210
|
-
{{
|
|
211
|
-
"name": "search",
|
|
212
|
-
"arguments": {{ "query": "latest population of Tokyo" }}
|
|
213
|
-
}}
|
|
214
|
-
</tool_call>
|
|
215
|
-
```
|
|
222
|
+
- **When:** If the query requires external data, calculations, or functions listed under AVAILABLE TOOLS.
|
|
223
|
+
- **Action:** Output *ONLY* the complete JSON tool call within tags.
|
|
224
|
+
- **Format:** Must start with `<tool_call>` and end with `</tool_call>`.
|
|
216
225
|
|
|
217
226
|
**2. Conversational Mode:**
|
|
218
|
-
- **When:**
|
|
219
|
-
- **Action:** Respond directly
|
|
220
|
-
- **Example:** *User:* "Explain photosynthesis." *Assistant:* "Photosynthesis is how plants use sunlight, water, and carbon dioxide to create their food (glucose) and release oxygen."
|
|
221
|
-
|
|
222
|
-
**ABSOLUTE PROHIBITIONS:**
|
|
223
|
-
- **NEVER Explain Tool Use:** Don't say you're using a tool, which one, or why.
|
|
224
|
-
- **NEVER Describe JSON/Tags:** Do not mention `tool_call`, JSON structure, or parameters.
|
|
225
|
-
- **NEVER Apologize for Tools:** No need to say sorry for lacking direct info.
|
|
226
|
-
- **NEVER Mix Text and Tool Calls:** Tool calls must be standalone.
|
|
227
|
-
|
|
228
|
-
**Be concise and relevant in all responses.**
|
|
227
|
+
- **When:** For queries answerable with internal knowledge.
|
|
228
|
+
- **Action:** Respond directly and concisely.
|
|
229
229
|
|
|
230
230
|
**AVAILABLE TOOLS:**
|
|
231
231
|
{tools_description}
|
|
232
232
|
|
|
233
|
-
**TOOL FORMAT
|
|
233
|
+
**TOOL FORMAT:**
|
|
234
234
|
<tool_call>
|
|
235
235
|
{{
|
|
236
236
|
"name": "tool_name",
|
|
237
237
|
"arguments": {{
|
|
238
238
|
"param": "value"
|
|
239
|
-
/* Add other parameters as needed */
|
|
240
239
|
}}
|
|
241
240
|
}}
|
|
242
241
|
</tool_call>
|
|
242
|
+
'''
|
|
243
|
+
|
|
244
|
+
def _generate_history_string(self) -> str:
|
|
245
|
+
"""Generate formatted history string from messages."""
|
|
246
|
+
history_parts = []
|
|
247
|
+
for msg in self.messages:
|
|
248
|
+
if msg.role == "system" and msg.metadata.get("summarized_count"):
|
|
249
|
+
history_parts.append(f"[Previous messages summarized: {msg.metadata['summarized_count']}]")
|
|
250
|
+
else:
|
|
251
|
+
role_display = msg.role.capitalize()
|
|
252
|
+
if "<tool_call>" in msg.content:
|
|
253
|
+
history_parts.append(f"{role_display}: {msg.content}")
|
|
254
|
+
else:
|
|
255
|
+
history_parts.append(f"{role_display}: {msg.content}")
|
|
256
|
+
return "\n".join(history_parts)
|
|
243
257
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
3. Avoid *all* prohibited explanations/text.
|
|
248
|
-
''')
|
|
249
|
-
|
|
250
|
-
incomplete_chat_history = self.chat_history + "\nUser: " + prompt + "\nAssistant:" # Ensure it ends correctly
|
|
251
|
-
complete_prompt = intro + self.__trim_chat_history(incomplete_chat_history, intro)
|
|
252
|
-
return complete_prompt
|
|
253
|
-
|
|
254
|
-
def update_chat_history(
|
|
255
|
-
self, prompt: str, response: str, force: bool = False
|
|
256
|
-
) -> None:
|
|
257
|
-
"""Keep the conversation flowing by updating the chat history!
|
|
258
|
-
|
|
259
|
-
This method:
|
|
260
|
-
- Adds new messages to the history
|
|
261
|
-
- Updates the file if needed
|
|
262
|
-
- Keeps everything organized
|
|
263
|
-
|
|
264
|
-
Args:
|
|
265
|
-
prompt (str): Your message to add
|
|
266
|
-
response (str): The LLM's response
|
|
267
|
-
force (bool): Force update even if history is off. Default: False
|
|
268
|
-
|
|
269
|
-
Examples:
|
|
270
|
-
>>> chat = Conversation()
|
|
271
|
-
>>> chat.update_chat_history("Hi!", "Hello there!")
|
|
272
|
-
"""
|
|
273
|
-
if not self.status and not force:
|
|
274
|
-
return
|
|
275
|
-
|
|
276
|
-
# Use the updated history_format
|
|
277
|
-
new_history = self.history_format % {"user": prompt, "llm": response}
|
|
258
|
+
def _trim_chat_history(self, chat_history: str, intro: str) -> str:
|
|
259
|
+
"""Trim chat history with improved token management."""
|
|
260
|
+
total_length = len(intro) + len(chat_history)
|
|
278
261
|
|
|
279
|
-
if
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
self.chat_history += new_history
|
|
290
|
-
# logger.info(f"Chat history updated with prompt: {prompt}")
|
|
291
|
-
|
|
292
|
-
def update_chat_history_with_tool(
|
|
293
|
-
self, prompt: str, tool_call_json: str, tool_result: str, force: bool = False # Changed tool_name to tool_call_json
|
|
294
|
-
) -> None:
|
|
295
|
-
"""Update chat history with a tool call and its result.
|
|
296
|
-
|
|
297
|
-
This method:
|
|
298
|
-
- Adds tool call interaction to the history using the new format
|
|
299
|
-
- Updates the file if needed
|
|
300
|
-
- Maintains the conversation flow with tools
|
|
262
|
+
if total_length > self.history_offset:
|
|
263
|
+
truncate_at = (total_length - self.history_offset) + self.prompt_allowance
|
|
264
|
+
# Try to truncate at a message boundary
|
|
265
|
+
lines = chat_history[truncate_at:].split('\n')
|
|
266
|
+
for i, line in enumerate(lines):
|
|
267
|
+
if line.startswith(("User:", "Assistant:", "Tool:")):
|
|
268
|
+
return "... " + "\n".join(lines[i:])
|
|
269
|
+
return "... " + chat_history[truncate_at:]
|
|
270
|
+
return chat_history
|
|
301
271
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
"
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
if self.file and self.update_file:
|
|
324
|
-
# Create file if it doesn't exist
|
|
272
|
+
def add_message(self, role: str, content: str, metadata: Optional[Dict[str, Any]] = None) -> None:
|
|
273
|
+
"""Add a message with enhanced validation and metadata support."""
|
|
274
|
+
try:
|
|
275
|
+
if not self.validate_message(role, content):
|
|
276
|
+
raise MessageValidationError("Invalid message role or content")
|
|
277
|
+
|
|
278
|
+
message = Message(role=role, content=content, metadata=metadata or {})
|
|
279
|
+
self.messages.append(message)
|
|
280
|
+
|
|
281
|
+
if self.file and self.update_file:
|
|
282
|
+
self._append_to_file(message)
|
|
283
|
+
|
|
284
|
+
self._compress_history()
|
|
285
|
+
|
|
286
|
+
except Exception as e:
|
|
287
|
+
self.logger.error(f"Error adding message: {str(e)}")
|
|
288
|
+
raise ConversationError(f"Failed to add message: {str(e)}") from e
|
|
289
|
+
|
|
290
|
+
def _append_to_file(self, message: Message) -> None:
|
|
291
|
+
"""Append message to file with error handling."""
|
|
292
|
+
try:
|
|
325
293
|
if not os.path.exists(self.file):
|
|
326
294
|
with open(self.file, "w", encoding="utf-8") as fh:
|
|
327
295
|
fh.write(self.intro + "\n")
|
|
328
296
|
|
|
329
|
-
# Append new history
|
|
330
297
|
with open(self.file, "a", encoding="utf-8") as fh:
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
This method:
|
|
339
|
-
- Validates the message role
|
|
340
|
-
- Adds the message to history
|
|
341
|
-
- Updates file if needed
|
|
342
|
-
|
|
343
|
-
Args:
|
|
344
|
-
role (str): Who's sending? ('user', 'llm', 'tool', or 'reasoning')
|
|
345
|
-
content (str): What's the message?
|
|
346
|
-
|
|
347
|
-
Examples:
|
|
348
|
-
>>> chat = Conversation()
|
|
349
|
-
>>> chat.add_message("user", "Hey there!")
|
|
350
|
-
>>> chat.add_message("llm", "Hi! How can I help?")
|
|
351
|
-
"""
|
|
352
|
-
if not self.validate_message(role, content):
|
|
353
|
-
raise ValueError("Invalid message role or content")
|
|
354
|
-
|
|
355
|
-
# Updated role formats to match User/Assistant
|
|
356
|
-
role_formats = {
|
|
357
|
-
"user": "User",
|
|
358
|
-
"assistant": "Assistant", # Changed from 'llm'
|
|
359
|
-
"llm": "Assistant", # Keep llm for backward compatibility? Or remove? Let's keep for now.
|
|
360
|
-
"tool": "Tool",
|
|
361
|
-
"reasoning": "Reasoning" # Keep reasoning if used internally
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if role in role_formats:
|
|
365
|
-
# Special handling for assistant's tool call output
|
|
366
|
-
if role == "assistant" and "<tool_call>" in content:
|
|
367
|
-
# History format already includes the tags, just add the content
|
|
368
|
-
self.chat_history += f"\n{role_formats[role]}: {content}"
|
|
369
|
-
elif role == "tool":
|
|
370
|
-
# Tool results follow the Assistant's tool call
|
|
371
|
-
self.chat_history += f"\n{role_formats[role]}: {content}"
|
|
372
|
-
else:
|
|
373
|
-
# Standard user/assistant message
|
|
374
|
-
self.chat_history += f"\n{role_formats[role]}: {content}"
|
|
375
|
-
else:
|
|
376
|
-
raise ValueError(f"Invalid role: {role}. Must be one of {list(role_formats.keys())}")
|
|
298
|
+
role_display = message.role.capitalize()
|
|
299
|
+
fh.write(f"\n{role_display}: {message.content}")
|
|
300
|
+
|
|
301
|
+
except Exception as e:
|
|
302
|
+
self.logger.error(f"Error writing to file: {str(e)}")
|
|
303
|
+
raise ConversationError(f"Failed to write to file: {str(e)}") from e
|
|
377
304
|
|
|
378
305
|
def validate_message(self, role: str, content: str) -> bool:
|
|
379
|
-
"""Validate
|
|
380
|
-
|
|
381
|
-
valid_roles = {'user', 'assistant', 'llm', 'tool', 'reasoning'} # Changed 'llm' to 'assistant', kept 'llm' maybe?
|
|
306
|
+
"""Validate message with enhanced role checking."""
|
|
307
|
+
valid_roles = {'user', 'assistant', 'tool', 'system'}
|
|
382
308
|
if role not in valid_roles:
|
|
383
|
-
|
|
309
|
+
self.logger.error(f"Invalid role: {role}")
|
|
384
310
|
return False
|
|
385
|
-
if not content:
|
|
386
|
-
|
|
311
|
+
if not content or not isinstance(content, str):
|
|
312
|
+
self.logger.error("Invalid content")
|
|
387
313
|
return False
|
|
388
314
|
return True
|
|
389
315
|
|
|
390
|
-
def
|
|
391
|
-
"""
|
|
316
|
+
def handle_tool_response(self, response: str) -> Dict[str, Any]:
|
|
317
|
+
"""Process tool responses with enhanced error handling."""
|
|
318
|
+
try:
|
|
319
|
+
if "<tool_call>" in response:
|
|
320
|
+
function_call_data = self._parse_function_call(response)
|
|
321
|
+
|
|
322
|
+
if "error" in function_call_data:
|
|
323
|
+
return {
|
|
324
|
+
"is_tool_call": True,
|
|
325
|
+
"success": False,
|
|
326
|
+
"result": function_call_data["error"],
|
|
327
|
+
"original_response": response
|
|
328
|
+
}
|
|
392
329
|
|
|
393
|
-
|
|
394
|
-
|
|
330
|
+
result = self.execute_function(function_call_data)
|
|
331
|
+
self.add_message("tool", result)
|
|
395
332
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
333
|
+
return {
|
|
334
|
+
"is_tool_call": True,
|
|
335
|
+
"success": True,
|
|
336
|
+
"result": result,
|
|
337
|
+
"tool_calls": function_call_data.get("tool_calls", []),
|
|
338
|
+
"original_response": response
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
"is_tool_call": False,
|
|
343
|
+
"result": response,
|
|
344
|
+
"original_response": response
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
except Exception as e:
|
|
348
|
+
self.logger.error(f"Error handling tool response: {str(e)}")
|
|
349
|
+
raise ToolCallError(f"Failed to handle tool response: {str(e)}") from e
|
|
350
|
+
|
|
351
|
+
def _parse_function_call(self, response: str) -> FunctionCallData:
|
|
352
|
+
"""Parse function calls with improved JSON handling."""
|
|
399
353
|
try:
|
|
400
|
-
#
|
|
401
|
-
start_tag
|
|
402
|
-
end_tag
|
|
403
|
-
start_idx
|
|
404
|
-
end_idx
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
# Try to parse the JSON directly
|
|
420
|
-
try:
|
|
421
|
-
parsed_response: Any = json.loads(json_str)
|
|
422
|
-
if isinstance(parsed_response, dict):
|
|
423
|
-
return {"tool_calls": [parsed_response]}
|
|
424
|
-
else:
|
|
425
|
-
raise ValueError("Invalid JSON structure in tool call.")
|
|
426
|
-
except json.JSONDecodeError:
|
|
427
|
-
# If direct parsing failed, try to extract just the JSON object
|
|
428
|
-
import re
|
|
429
|
-
json_pattern = re.search(r'\{[\s\S]*\}', json_str)
|
|
430
|
-
if json_pattern:
|
|
431
|
-
parsed_response = json.loads(json_pattern.group(0))
|
|
432
|
-
return {"tool_calls": [parsed_response]}
|
|
433
|
-
raise
|
|
434
|
-
else:
|
|
435
|
-
# Extract JSON content - for the format with brackets
|
|
436
|
-
json_str: str = response[start_idx + len(start_tag):end_idx].strip()
|
|
437
|
-
parsed_response: Any = json.loads(json_str)
|
|
438
|
-
|
|
439
|
-
if isinstance(parsed_response, list):
|
|
440
|
-
return {"tool_calls": parsed_response}
|
|
441
|
-
elif isinstance(parsed_response, dict):
|
|
442
|
-
return {"tool_calls": [parsed_response]}
|
|
354
|
+
# Extract content between tool call tags
|
|
355
|
+
start_tag = "<tool_call>"
|
|
356
|
+
end_tag = "</tool_call>"
|
|
357
|
+
start_idx = response.find(start_tag)
|
|
358
|
+
end_idx = response.rfind(end_tag)
|
|
359
|
+
|
|
360
|
+
if start_idx == -1 or end_idx == -1:
|
|
361
|
+
raise ValueError("No valid tool call tags found")
|
|
362
|
+
|
|
363
|
+
json_str = response[start_idx + len(start_tag):end_idx].strip()
|
|
364
|
+
|
|
365
|
+
# Handle both single and multiple tool calls
|
|
366
|
+
try:
|
|
367
|
+
parsed = json.loads(json_str)
|
|
368
|
+
if isinstance(parsed, dict):
|
|
369
|
+
return {"tool_calls": [parsed]}
|
|
370
|
+
elif isinstance(parsed, list):
|
|
371
|
+
return {"tool_calls": parsed}
|
|
443
372
|
else:
|
|
444
|
-
raise ValueError("
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
373
|
+
raise ValueError("Invalid tool call structure")
|
|
374
|
+
except json.JSONDecodeError:
|
|
375
|
+
# Try to extract valid JSON if embedded in other content
|
|
376
|
+
import re
|
|
377
|
+
json_pattern = re.search(r'\{[\s\S]*\}', json_str)
|
|
378
|
+
if json_pattern:
|
|
379
|
+
parsed = json.loads(json_pattern.group(0))
|
|
380
|
+
return {"tool_calls": [parsed]}
|
|
381
|
+
raise
|
|
382
|
+
|
|
383
|
+
except Exception as e:
|
|
384
|
+
self.logger.error(f"Error parsing function call: {str(e)}")
|
|
448
385
|
return {"error": str(e)}
|
|
449
386
|
|
|
450
387
|
def execute_function(self, function_call_data: FunctionCallData) -> str:
|
|
451
|
-
"""Execute
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
arguments: Dict[str, Any] = tool_call.get("arguments", {})
|
|
468
|
-
|
|
469
|
-
if not function_name or not isinstance(arguments, dict):
|
|
470
|
-
results.append(f"Invalid tool call: {tool_call}")
|
|
471
|
-
continue
|
|
472
|
-
|
|
473
|
-
# Here you would implement the actual execution logic for each tool
|
|
474
|
-
# For demonstration, we'll return a placeholder response
|
|
475
|
-
results.append(f"Executed {function_name} with arguments {arguments}")
|
|
476
|
-
|
|
477
|
-
return "; ".join(results)
|
|
478
|
-
|
|
479
|
-
def _convert_fns_to_tools(self, fns: Optional[List[Fn]]) -> List[ToolDefinition]:
|
|
480
|
-
"""Convert functions to tool definitions for the LLM.
|
|
388
|
+
"""Execute functions with enhanced error handling."""
|
|
389
|
+
try:
|
|
390
|
+
tool_calls = function_call_data.get("tool_calls", [])
|
|
391
|
+
if not tool_calls:
|
|
392
|
+
raise ValueError("No tool calls provided")
|
|
393
|
+
|
|
394
|
+
results = []
|
|
395
|
+
for tool_call in tool_calls:
|
|
396
|
+
name = tool_call.get("name")
|
|
397
|
+
arguments = tool_call.get("arguments", {})
|
|
398
|
+
|
|
399
|
+
if not name or not isinstance(arguments, dict):
|
|
400
|
+
raise ValueError(f"Invalid tool call format: {tool_call}")
|
|
401
|
+
|
|
402
|
+
# Execute the tool (implement actual logic here)
|
|
403
|
+
results.append(f"Executed {name} with arguments {arguments}")
|
|
481
404
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
"""
|
|
488
|
-
if not fns:
|
|
489
|
-
return []
|
|
490
|
-
|
|
491
|
-
tools: List[ToolDefinition] = []
|
|
492
|
-
for fn in fns:
|
|
493
|
-
tool: ToolDefinition = {
|
|
494
|
-
"type": "function",
|
|
495
|
-
"function": {
|
|
496
|
-
"name": fn.name,
|
|
497
|
-
"description": fn.description,
|
|
498
|
-
"parameters": {
|
|
499
|
-
"type": "object",
|
|
500
|
-
"properties": {
|
|
501
|
-
param_name: {
|
|
502
|
-
"type": param_type,
|
|
503
|
-
"description": f"The {param_name} parameter"
|
|
504
|
-
} for param_name, param_type in fn.parameters.items()
|
|
505
|
-
},
|
|
506
|
-
"required": list(fn.parameters.keys())
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
tools.append(tool)
|
|
511
|
-
return tools
|
|
405
|
+
return "; ".join(results)
|
|
406
|
+
|
|
407
|
+
except Exception as e:
|
|
408
|
+
self.logger.error(f"Error executing function: {str(e)}")
|
|
409
|
+
raise ToolCallError(f"Failed to execute function: {str(e)}") from e
|
|
512
410
|
|
|
513
411
|
def get_tools_description(self) -> str:
|
|
514
|
-
"""Get
|
|
515
|
-
|
|
516
|
-
Returns:
|
|
517
|
-
str: Formatted tools description
|
|
518
|
-
"""
|
|
412
|
+
"""Get formatted tools description."""
|
|
519
413
|
if not self.tools:
|
|
520
414
|
return ""
|
|
521
415
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
return "\n".join(tools_desc)
|
|
528
|
-
|
|
529
|
-
def handle_tool_response(self, response: str) -> Dict[str, Any]:
|
|
530
|
-
"""Process a response that might contain a tool call.
|
|
531
|
-
|
|
532
|
-
This method:
|
|
533
|
-
- Checks if the response contains a tool call
|
|
534
|
-
- Parses and executes the tool call if present
|
|
535
|
-
- Returns the appropriate result
|
|
416
|
+
return "\n".join(
|
|
417
|
+
f"- {fn.name}: {fn.description} (Parameters: {', '.join(f'{name}: {typ}' for name, typ in fn.parameters.items())})"
|
|
418
|
+
for fn in self.tools
|
|
419
|
+
)
|
|
536
420
|
|
|
421
|
+
def update_chat_history(self, prompt: str, response: str) -> None:
|
|
422
|
+
"""Update chat history with a new prompt-response pair.
|
|
423
|
+
|
|
537
424
|
Args:
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
425
|
+
prompt: The user's prompt/question
|
|
426
|
+
response: The assistant's response
|
|
427
|
+
|
|
428
|
+
This method adds both the user's prompt and the assistant's response
|
|
429
|
+
to the conversation history as separate messages.
|
|
542
430
|
"""
|
|
543
|
-
#
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
return {
|
|
549
|
-
"is_tool_call": True,
|
|
550
|
-
"success": False,
|
|
551
|
-
"result": function_call_data["error"],
|
|
552
|
-
"original_response": response
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
# Execute the function call
|
|
556
|
-
result = self.execute_function(function_call_data)
|
|
557
|
-
|
|
558
|
-
# Add the result to chat history as a tool message
|
|
559
|
-
# The assistant's response (the tool call itself) should have been added before calling this
|
|
560
|
-
# Now we add the tool's result
|
|
561
|
-
self.add_message("tool", result) # This will now correctly add "\nTool: <result>"
|
|
562
|
-
|
|
563
|
-
return {
|
|
564
|
-
"is_tool_call": True,
|
|
565
|
-
"success": True,
|
|
566
|
-
"result": result, # This is the tool's execution result
|
|
567
|
-
"tool_calls": function_call_data.get("tool_calls", []),
|
|
568
|
-
"original_response": response # This is the LLM's response containing the <tool_call>
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
return {
|
|
572
|
-
"is_tool_call": False,
|
|
573
|
-
"result": response,
|
|
574
|
-
"original_response": response
|
|
575
|
-
}
|
|
576
|
-
|
|
431
|
+
# Add user's message
|
|
432
|
+
self.add_message("user", prompt)
|
|
433
|
+
|
|
434
|
+
# Add assistant's response
|
|
435
|
+
self.add_message("assistant", response)
|
|
577
436
|
|