sonika-langchain-bot 0.0.11__py3-none-any.whl → 0.0.12__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.
- sonika_langchain_bot/langchain_bot_agent.py +633 -192
- {sonika_langchain_bot-0.0.11.dist-info → sonika_langchain_bot-0.0.12.dist-info}/METADATA +2 -2
- {sonika_langchain_bot-0.0.11.dist-info → sonika_langchain_bot-0.0.12.dist-info}/RECORD +6 -6
- {sonika_langchain_bot-0.0.11.dist-info → sonika_langchain_bot-0.0.12.dist-info}/WHEEL +0 -0
- {sonika_langchain_bot-0.0.11.dist-info → sonika_langchain_bot-0.0.12.dist-info}/licenses/LICENSE +0 -0
- {sonika_langchain_bot-0.0.11.dist-info → sonika_langchain_bot-0.0.12.dist-info}/top_level.txt +0 -0
@@ -1,232 +1,422 @@
|
|
1
|
-
from typing import Generator, List
|
2
|
-
|
1
|
+
from typing import Generator, List, Optional, Dict, Any, TypedDict, Annotated
|
2
|
+
import asyncio
|
3
3
|
from langchain.schema import AIMessage, HumanMessage, BaseMessage
|
4
|
+
from langchain_core.messages import ToolMessage
|
4
5
|
from langchain.text_splitter import CharacterTextSplitter
|
5
6
|
from langchain_community.vectorstores import FAISS
|
6
|
-
from sonika_langchain_bot.langchain_class import FileProcessorInterface, IEmbeddings, ILanguageModel, Message, ResponseModel
|
7
|
-
from langgraph.checkpoint.memory import MemorySaver
|
8
|
-
from langgraph.prebuilt import create_react_agent
|
9
7
|
from langchain_community.tools import BaseTool
|
10
|
-
import
|
8
|
+
from langgraph.graph import StateGraph, END, add_messages
|
9
|
+
from langgraph.prebuilt import ToolNode
|
10
|
+
from langgraph.checkpoint.memory import MemorySaver
|
11
|
+
from langchain_mcp_adapters.client import MultiServerMCPClient
|
12
|
+
|
13
|
+
# Import your existing interfaces
|
14
|
+
from sonika_langchain_bot.langchain_class import FileProcessorInterface, IEmbeddings, ILanguageModel, Message, ResponseModel
|
15
|
+
|
16
|
+
|
17
|
+
class ChatState(TypedDict):
|
18
|
+
"""
|
19
|
+
Modern chat state for LangGraph workflow.
|
20
|
+
|
21
|
+
Attributes:
|
22
|
+
messages: List of conversation messages with automatic message handling
|
23
|
+
context: Contextual information from processed files
|
24
|
+
"""
|
25
|
+
messages: Annotated[List[BaseMessage], add_messages]
|
26
|
+
context: str
|
27
|
+
|
11
28
|
|
12
29
|
class LangChainBot:
|
13
30
|
"""
|
14
|
-
|
15
|
-
|
31
|
+
Modern LangGraph-based conversational bot with MCP support.
|
32
|
+
|
33
|
+
This implementation provides 100% API compatibility with existing ChatService
|
34
|
+
while using modern LangGraph workflows and native tool calling internally.
|
35
|
+
|
36
|
+
Features:
|
37
|
+
- Native tool calling (no manual parsing)
|
38
|
+
- MCP (Model Context Protocol) support
|
39
|
+
- File processing with vector search
|
40
|
+
- Thread-based conversation persistence
|
41
|
+
- Streaming responses
|
42
|
+
- Backward compatibility with legacy APIs
|
16
43
|
"""
|
17
44
|
|
18
|
-
def __init__(self,
|
45
|
+
def __init__(self,
|
46
|
+
language_model: ILanguageModel,
|
47
|
+
embeddings: IEmbeddings,
|
48
|
+
instructions: str,
|
49
|
+
tools: Optional[List[BaseTool]] = None,
|
50
|
+
mcp_servers: Optional[Dict[str, Any]] = None,
|
51
|
+
use_checkpointer: bool = False):
|
19
52
|
"""
|
20
|
-
|
53
|
+
Initialize the modern LangGraph bot with optional MCP support.
|
21
54
|
|
22
55
|
Args:
|
23
|
-
language_model (ILanguageModel):
|
24
|
-
embeddings (IEmbeddings):
|
25
|
-
instructions (str):
|
26
|
-
tools (List[BaseTool]):
|
56
|
+
language_model (ILanguageModel): The language model to use for generation
|
57
|
+
embeddings (IEmbeddings): Embedding model for file processing and context retrieval
|
58
|
+
instructions (str): System instructions that will be modernized automatically
|
59
|
+
tools (List[BaseTool], optional): Traditional LangChain tools to bind to the model
|
60
|
+
mcp_servers (Dict[str, Any], optional): MCP server configurations for dynamic tool loading
|
61
|
+
use_checkpointer (bool): Enable automatic conversation persistence using LangGraph checkpoints
|
62
|
+
|
63
|
+
Note:
|
64
|
+
The instructions will be automatically enhanced with tool descriptions
|
65
|
+
when tools are provided, eliminating the need for manual tool instruction formatting.
|
27
66
|
"""
|
67
|
+
# Core components
|
28
68
|
self.language_model = language_model
|
29
69
|
self.embeddings = embeddings
|
30
|
-
|
70
|
+
self.base_instructions = instructions
|
71
|
+
|
72
|
+
# Backward compatibility attributes
|
31
73
|
self.chat_history: List[BaseMessage] = []
|
32
|
-
self.memory_agent = MemorySaver()
|
33
74
|
self.vector_store = None
|
34
|
-
self.tools = tools
|
35
|
-
self.instructions = instructions
|
36
|
-
self.add_tools_to_instructions(tools)
|
37
|
-
self.conversation = self._create_conversation_chain()
|
38
|
-
self.agent_executor = self._create_agent_executor()
|
39
|
-
|
40
|
-
def add_tools_to_instructions(self, tools: List[BaseTool]):
|
41
|
-
"""Agrega información de las herramientas a las instrucciones base del sistema."""
|
42
|
-
if len(tools) == 0:
|
43
|
-
return
|
44
|
-
|
45
|
-
# Instrucciones sobre el uso de herramientas
|
46
|
-
tools_instructions = '''\n\nWhen you want to execute a tool, enclose the command with three asterisks and provide all parameters needed.
|
47
|
-
Ensure you gather all relevant information from the conversation to use the parameters.
|
48
|
-
If information is missing, search online.
|
49
|
-
|
50
|
-
This is a list of the tools you can execute:
|
51
|
-
'''
|
52
|
-
|
53
|
-
# Procesar cada herramienta y agregarla a las instrucciones
|
54
|
-
for tool in tools:
|
55
|
-
tool_name = tool.name
|
56
|
-
tool_description = tool.description
|
57
|
-
|
58
|
-
tools_instructions += f"\nTool Name: {tool_name}\n"
|
59
|
-
tools_instructions += f"Description: {tool_description}\n"
|
60
|
-
|
61
|
-
# Intentar obtener información de parámetros
|
62
|
-
run_method = getattr(tool, '_run', None)
|
63
|
-
if run_method:
|
64
|
-
try:
|
65
|
-
import inspect
|
66
|
-
params = inspect.signature(run_method)
|
67
|
-
tools_instructions += f"Parameters: {params}\n"
|
68
|
-
except:
|
69
|
-
tools_instructions += "Parameters: Not available\n"
|
70
|
-
else:
|
71
|
-
tools_instructions += "Parameters: Not available\n"
|
72
|
-
|
73
|
-
tools_instructions += "---\n"
|
74
75
|
|
75
|
-
#
|
76
|
-
self.
|
76
|
+
# Tool configuration
|
77
|
+
self.tools = tools or []
|
78
|
+
self.mcp_client = None
|
79
|
+
|
80
|
+
# Initialize MCP servers if provided
|
81
|
+
if mcp_servers:
|
82
|
+
self._initialize_mcp(mcp_servers)
|
83
|
+
|
84
|
+
# Configure persistence layer
|
85
|
+
self.checkpointer = MemorySaver() if use_checkpointer else None
|
86
|
+
|
87
|
+
# Prepare model with bound tools for native function calling
|
88
|
+
self.model_with_tools = self._prepare_model_with_tools()
|
77
89
|
|
90
|
+
# Build modern instruction set with tool descriptions
|
91
|
+
self.instructions = self._build_modern_instructions()
|
92
|
+
|
93
|
+
# Create the LangGraph workflow
|
94
|
+
self.graph = self._create_modern_workflow()
|
95
|
+
|
96
|
+
# Legacy compatibility attributes (maintained for API compatibility)
|
97
|
+
self.conversation = None
|
98
|
+
self.agent_executor = None
|
78
99
|
|
79
|
-
def
|
100
|
+
def _initialize_mcp(self, mcp_servers: Dict[str, Any]):
|
80
101
|
"""
|
81
|
-
|
102
|
+
Initialize MCP (Model Context Protocol) connections and load available tools.
|
103
|
+
|
104
|
+
This method establishes connections to configured MCP servers and automatically
|
105
|
+
imports their tools into the bot's tool collection.
|
106
|
+
|
107
|
+
Args:
|
108
|
+
mcp_servers (Dict[str, Any]): Dictionary of MCP server configurations
|
109
|
+
Example: {
|
110
|
+
"server_name": {
|
111
|
+
"command": "python",
|
112
|
+
"args": ["/path/to/server.py"],
|
113
|
+
"transport": "stdio"
|
114
|
+
}
|
115
|
+
}
|
116
|
+
|
117
|
+
Note:
|
118
|
+
MCP tools are automatically appended to the existing tools list and
|
119
|
+
will be included in the model's tool binding process.
|
82
120
|
"""
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
def _create_agent_executor(self):
|
121
|
+
try:
|
122
|
+
self.mcp_client = MultiServerMCPClient(mcp_servers)
|
123
|
+
mcp_tools = asyncio.run(self.mcp_client.get_tools())
|
124
|
+
self.tools.extend(mcp_tools)
|
125
|
+
print(f"✅ MCP initialized: {len(mcp_tools)} tools from {len(mcp_servers)} servers")
|
126
|
+
except Exception as e:
|
127
|
+
print(f"⚠️ MCP initialization error: {e}")
|
128
|
+
self.mcp_client = None
|
129
|
+
|
130
|
+
def _prepare_model_with_tools(self):
|
95
131
|
"""
|
96
|
-
|
97
|
-
|
132
|
+
Prepare the language model with bound tools for native function calling.
|
133
|
+
|
134
|
+
This method binds all available tools (both traditional and MCP) to the language model,
|
135
|
+
enabling native function calling without manual parsing or instruction formatting.
|
136
|
+
|
98
137
|
Returns:
|
99
|
-
|
138
|
+
The language model with tools bound, or the original model if no tools are available
|
100
139
|
"""
|
101
|
-
|
140
|
+
if self.tools:
|
141
|
+
return self.language_model.model.bind_tools(self.tools)
|
142
|
+
return self.language_model.model
|
102
143
|
|
103
|
-
def
|
144
|
+
def _build_modern_instructions(self) -> str:
|
104
145
|
"""
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
146
|
+
Build modern system instructions with automatic tool descriptions.
|
147
|
+
|
148
|
+
This method enhances the base instructions with professional tool descriptions
|
149
|
+
that leverage native function calling capabilities, eliminating the need for
|
150
|
+
manual tool instruction formatting.
|
151
|
+
|
110
152
|
Returns:
|
111
|
-
str:
|
153
|
+
str: Complete system instructions including tool descriptions
|
112
154
|
"""
|
113
|
-
|
114
|
-
|
115
|
-
|
155
|
+
instructions = self.base_instructions
|
156
|
+
|
157
|
+
if self.tools:
|
158
|
+
tools_description = "\n\nYou have access to the following tools:\n"
|
159
|
+
for tool in self.tools:
|
160
|
+
tools_description += f"- {tool.name}: {tool.description}\n"
|
161
|
+
|
162
|
+
tools_description += ("\nCall these tools when needed using the standard function calling format. "
|
163
|
+
"You can call multiple tools in sequence if necessary to fully answer the user's question.")
|
164
|
+
|
165
|
+
instructions += tools_description
|
166
|
+
|
167
|
+
return instructions
|
116
168
|
|
117
|
-
def
|
169
|
+
def _create_modern_workflow(self) -> StateGraph:
|
118
170
|
"""
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
171
|
+
Create a modern LangGraph workflow using idiomatic patterns.
|
172
|
+
|
173
|
+
This method constructs a state-based workflow that handles:
|
174
|
+
- Agent reasoning and response generation
|
175
|
+
- Automatic tool execution via ToolNode
|
176
|
+
- Context integration from processed files
|
177
|
+
- Error handling and fallback responses
|
178
|
+
|
124
179
|
Returns:
|
125
|
-
|
180
|
+
StateGraph: Compiled LangGraph workflow ready for execution
|
126
181
|
"""
|
127
|
-
context = self._get_context(user_input)
|
128
|
-
augmented_input = f"User question: {user_input}"
|
129
|
-
if context:
|
130
|
-
augmented_input = f"Context from attached files:\n{context}\n\nUser question: {user_input}"
|
131
|
-
|
132
|
-
# Usamos el historial de chat directamente
|
133
|
-
bot_response = self.conversation.invoke({
|
134
|
-
"input": augmented_input,
|
135
|
-
"history": self.chat_history
|
136
|
-
})
|
137
|
-
|
138
|
-
token_usage = bot_response.response_metadata.get('token_usage', {})
|
139
|
-
bot_response_content = bot_response.content
|
140
182
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
183
|
+
def agent_node(state: ChatState) -> ChatState:
|
184
|
+
"""
|
185
|
+
Main agent node responsible for generating responses and initiating tool calls.
|
186
|
+
|
187
|
+
This node:
|
188
|
+
1. Extracts the latest user message from the conversation state
|
189
|
+
2. Retrieves relevant context from processed files
|
190
|
+
3. Constructs a complete message history for the model
|
191
|
+
4. Invokes the model with tool binding for native function calling
|
192
|
+
5. Returns updated state with the model's response
|
193
|
+
|
194
|
+
Args:
|
195
|
+
state (ChatState): Current conversation state
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
ChatState: Updated state with agent response
|
199
|
+
"""
|
200
|
+
# Extract the most recent user message
|
201
|
+
last_user_message = None
|
202
|
+
for msg in reversed(state["messages"]):
|
203
|
+
if isinstance(msg, HumanMessage):
|
204
|
+
last_user_message = msg.content
|
205
|
+
break
|
206
|
+
|
207
|
+
if not last_user_message:
|
208
|
+
return state
|
209
|
+
|
210
|
+
# Retrieve contextual information from processed files
|
211
|
+
context = self._get_context(last_user_message)
|
212
|
+
|
213
|
+
# Build system prompt with optional context
|
214
|
+
system_content = self.instructions
|
215
|
+
if context:
|
216
|
+
system_content += f"\n\nContext from uploaded files:\n{context}"
|
217
|
+
|
218
|
+
# Construct message history in OpenAI format
|
219
|
+
messages = [{"role": "system", "content": system_content}]
|
220
|
+
|
221
|
+
# Add conversation history with simplified message handling
|
222
|
+
for msg in state["messages"]:
|
223
|
+
if isinstance(msg, HumanMessage):
|
224
|
+
messages.append({"role": "user", "content": msg.content})
|
225
|
+
elif isinstance(msg, AIMessage):
|
226
|
+
messages.append({"role": "assistant", "content": msg.content or ""})
|
227
|
+
elif isinstance(msg, ToolMessage):
|
228
|
+
# Convert tool results to user messages for context
|
229
|
+
messages.append({"role": "user", "content": f"Tool result: {msg.content}"})
|
230
|
+
|
231
|
+
try:
|
232
|
+
# Invoke model with native tool binding
|
233
|
+
response = self.model_with_tools.invoke(messages)
|
234
|
+
|
235
|
+
# Return updated state
|
236
|
+
return {
|
237
|
+
**state,
|
238
|
+
"context": context,
|
239
|
+
"messages": [response] # add_messages annotation handles proper appending
|
240
|
+
}
|
241
|
+
|
242
|
+
except Exception as e:
|
243
|
+
print(f"Error in agent_node: {e}")
|
244
|
+
# Graceful fallback for error scenarios
|
245
|
+
fallback_response = AIMessage(content="I apologize, but I encountered an error processing your request.")
|
246
|
+
return {
|
247
|
+
**state,
|
248
|
+
"context": context,
|
249
|
+
"messages": [fallback_response]
|
250
|
+
}
|
251
|
+
|
252
|
+
def should_continue(state: ChatState) -> str:
|
253
|
+
"""
|
254
|
+
Conditional edge function to determine workflow continuation.
|
255
|
+
|
256
|
+
Analyzes the last message to decide whether to execute tools or end the workflow.
|
257
|
+
This leverages LangGraph's native tool calling detection.
|
258
|
+
|
259
|
+
Args:
|
260
|
+
state (ChatState): Current conversation state
|
261
|
+
|
262
|
+
Returns:
|
263
|
+
str: Next node to execute ("tools" or "end")
|
264
|
+
"""
|
265
|
+
last_message = state["messages"][-1]
|
266
|
+
|
267
|
+
# Check for pending tool calls using native tool calling detection
|
268
|
+
if (isinstance(last_message, AIMessage) and
|
269
|
+
hasattr(last_message, 'tool_calls') and
|
270
|
+
last_message.tool_calls):
|
271
|
+
return "tools"
|
272
|
+
|
273
|
+
return "end"
|
147
274
|
|
148
|
-
|
149
|
-
|
275
|
+
# Construct the workflow graph
|
276
|
+
workflow = StateGraph(ChatState)
|
277
|
+
|
278
|
+
# Add primary agent node
|
279
|
+
workflow.add_node("agent", agent_node)
|
280
|
+
|
281
|
+
# Add tool execution node if tools are available
|
282
|
+
if self.tools:
|
283
|
+
# ToolNode automatically handles tool execution and result formatting
|
284
|
+
tool_node = ToolNode(self.tools)
|
285
|
+
workflow.add_node("tools", tool_node)
|
286
|
+
|
287
|
+
# Define workflow edges and entry point
|
288
|
+
workflow.set_entry_point("agent")
|
289
|
+
|
290
|
+
if self.tools:
|
291
|
+
# Conditional routing based on tool call presence
|
292
|
+
workflow.add_conditional_edges(
|
293
|
+
"agent",
|
294
|
+
should_continue,
|
295
|
+
{
|
296
|
+
"tools": "tools",
|
297
|
+
"end": END
|
298
|
+
}
|
150
299
|
)
|
300
|
+
# Return to agent after tool execution for final response formatting
|
301
|
+
workflow.add_edge("tools", "agent")
|
302
|
+
else:
|
303
|
+
# Direct termination if no tools are available
|
304
|
+
workflow.add_edge("agent", END)
|
305
|
+
|
306
|
+
# Compile workflow with optional checkpointing
|
307
|
+
if self.checkpointer:
|
308
|
+
return workflow.compile(checkpointer=self.checkpointer)
|
309
|
+
else:
|
310
|
+
return workflow.compile()
|
151
311
|
|
152
|
-
|
153
|
-
agent_response = ""
|
154
|
-
|
155
|
-
for response in result_stream:
|
156
|
-
if 'tools' in response:
|
157
|
-
for message in response['tools']['messages']:
|
158
|
-
tool_response = message.content
|
159
|
-
if 'agent' in response:
|
160
|
-
for message in response['agent']['messages']:
|
161
|
-
agent_response = message.content
|
162
|
-
|
163
|
-
bot_response_content = agent_response if agent_response else tool_response
|
164
|
-
|
165
|
-
user_tokens = token_usage.get('prompt_tokens', 0)
|
166
|
-
bot_tokens = token_usage.get('completion_tokens', 0)
|
167
|
-
|
168
|
-
self.save_messages(user_input, bot_response_content)
|
169
|
-
|
170
|
-
return ResponseModel(user_tokens=user_tokens, bot_tokens=bot_tokens, response=bot_response_content)
|
312
|
+
# ===== LEGACY API COMPATIBILITY =====
|
171
313
|
|
172
|
-
def
|
173
|
-
"""
|
174
|
-
Genera una respuesta en streaming para la entrada del usuario, procesando el contexto.
|
175
|
-
|
176
|
-
Args:
|
177
|
-
user_input (str): Entrada del usuario
|
178
|
-
|
179
|
-
Yields:
|
180
|
-
str: Fragmentos de la respuesta generada por el modelo en tiempo real
|
314
|
+
def get_response(self, user_input: str) -> ResponseModel:
|
181
315
|
"""
|
182
|
-
|
183
|
-
augmented_input = f"User question: {user_input}"
|
184
|
-
if context:
|
185
|
-
augmented_input = f"Context from attached files:\n{context}\n\nUser question: {user_input}"
|
186
|
-
|
187
|
-
# Usamos el historial de chat directamente
|
188
|
-
result_stream = self.conversation.stream({
|
189
|
-
"input": augmented_input,
|
190
|
-
"history": self.chat_history
|
191
|
-
})
|
316
|
+
Generate a response while maintaining 100% API compatibility.
|
192
317
|
|
193
|
-
|
194
|
-
|
195
|
-
content = response.content
|
196
|
-
full_response += content
|
197
|
-
yield content
|
318
|
+
This method provides the primary interface for single-turn conversations,
|
319
|
+
maintaining backward compatibility with existing ChatService implementations.
|
198
320
|
|
199
|
-
# Guardamos los mensajes después del streaming
|
200
|
-
self.save_messages(user_input, full_response)
|
201
|
-
|
202
|
-
def _get_context(self, query: str) -> str:
|
203
|
-
"""
|
204
|
-
Obtiene el contexto relevante para una consulta del almacén de vectores.
|
205
|
-
|
206
321
|
Args:
|
207
|
-
|
208
|
-
|
322
|
+
user_input (str): The user's message or query
|
323
|
+
|
209
324
|
Returns:
|
210
|
-
|
325
|
+
ResponseModel: Structured response containing:
|
326
|
+
- user_tokens: Input token count
|
327
|
+
- bot_tokens: Output token count
|
328
|
+
- response: Generated response text
|
329
|
+
|
330
|
+
Note:
|
331
|
+
This method automatically handles tool execution and context integration
|
332
|
+
from processed files while maintaining the original API signature.
|
211
333
|
"""
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
334
|
+
# Prepare initial workflow state
|
335
|
+
initial_state = {
|
336
|
+
"messages": self.chat_history + [HumanMessage(content=user_input)],
|
337
|
+
"context": ""
|
338
|
+
}
|
339
|
+
|
340
|
+
# Execute the LangGraph workflow
|
341
|
+
result = self.graph.invoke(initial_state)
|
342
|
+
|
343
|
+
# Update internal conversation history
|
344
|
+
self.chat_history = result["messages"]
|
345
|
+
|
346
|
+
# Extract final response from the last assistant message
|
347
|
+
final_response = ""
|
348
|
+
total_input_tokens = 0
|
349
|
+
total_output_tokens = 0
|
350
|
+
|
351
|
+
for msg in reversed(result["messages"]):
|
352
|
+
if isinstance(msg, AIMessage) and msg.content:
|
353
|
+
final_response = msg.content
|
354
|
+
break
|
355
|
+
|
356
|
+
# Extract token usage from response metadata
|
357
|
+
last_message = result["messages"][-1]
|
358
|
+
if hasattr(last_message, 'response_metadata'):
|
359
|
+
token_usage = last_message.response_metadata.get('token_usage', {})
|
360
|
+
total_input_tokens = token_usage.get('prompt_tokens', 0)
|
361
|
+
total_output_tokens = token_usage.get('completion_tokens', 0)
|
362
|
+
|
363
|
+
return ResponseModel(
|
364
|
+
user_tokens=total_input_tokens,
|
365
|
+
bot_tokens=total_output_tokens,
|
366
|
+
response=final_response
|
367
|
+
)
|
368
|
+
|
369
|
+
def get_response_stream(self, user_input: str) -> Generator[str, None, None]:
|
218
370
|
"""
|
219
|
-
|
371
|
+
Generate a streaming response for real-time user interaction.
|
372
|
+
|
373
|
+
This method provides streaming capabilities while maintaining backward
|
374
|
+
compatibility with the original API.
|
375
|
+
|
376
|
+
Args:
|
377
|
+
user_input (str): The user's message or query
|
378
|
+
|
379
|
+
Yields:
|
380
|
+
str: Response chunks as they are generated
|
381
|
+
|
382
|
+
Note:
|
383
|
+
Current implementation streams complete responses. For token-level
|
384
|
+
streaming, consider using the model's native streaming capabilities.
|
220
385
|
"""
|
221
|
-
|
222
|
-
|
386
|
+
initial_state = {
|
387
|
+
"messages": self.chat_history + [HumanMessage(content=user_input)],
|
388
|
+
"context": ""
|
389
|
+
}
|
390
|
+
|
391
|
+
accumulated_response = ""
|
392
|
+
|
393
|
+
# Stream workflow execution
|
394
|
+
for chunk in self.graph.stream(initial_state):
|
395
|
+
# Extract content from workflow chunks
|
396
|
+
if "agent" in chunk:
|
397
|
+
for message in chunk["agent"]["messages"]:
|
398
|
+
if isinstance(message, AIMessage) and message.content:
|
399
|
+
# Stream complete responses (can be enhanced for token-level streaming)
|
400
|
+
accumulated_response = message.content
|
401
|
+
yield message.content
|
402
|
+
|
403
|
+
# Update conversation history after streaming completion
|
404
|
+
if accumulated_response:
|
405
|
+
self.chat_history.extend([
|
406
|
+
HumanMessage(content=user_input),
|
407
|
+
AIMessage(content=accumulated_response)
|
408
|
+
])
|
223
409
|
|
224
410
|
def load_conversation_history(self, messages: List[Message]):
|
225
411
|
"""
|
226
|
-
|
227
|
-
|
412
|
+
Load conversation history from Django model instances.
|
413
|
+
|
414
|
+
This method maintains compatibility with existing Django-based conversation
|
415
|
+
storage while preparing the history for modern LangGraph processing.
|
416
|
+
|
228
417
|
Args:
|
229
|
-
messages:
|
418
|
+
messages (List[Message]): List of Django Message model instances
|
419
|
+
Expected to have 'content' and 'is_bot' attributes
|
230
420
|
"""
|
231
421
|
self.chat_history.clear()
|
232
422
|
for message in messages:
|
@@ -237,45 +427,296 @@ This is a list of the tools you can execute:
|
|
237
427
|
|
238
428
|
def save_messages(self, user_message: str, bot_response: str):
|
239
429
|
"""
|
240
|
-
|
241
|
-
|
430
|
+
Save messages to internal conversation history.
|
431
|
+
|
432
|
+
This method provides backward compatibility for manual history management.
|
433
|
+
|
242
434
|
Args:
|
243
|
-
user_message (str):
|
244
|
-
bot_response (str):
|
435
|
+
user_message (str): The user's input message
|
436
|
+
bot_response (str): The bot's generated response
|
245
437
|
"""
|
246
438
|
self.chat_history.append(HumanMessage(content=user_message))
|
247
439
|
self.chat_history.append(AIMessage(content=bot_response))
|
248
440
|
|
249
441
|
def process_file(self, file: FileProcessorInterface):
|
250
442
|
"""
|
251
|
-
|
252
|
-
|
443
|
+
Process and index a file for contextual retrieval.
|
444
|
+
|
445
|
+
This method maintains compatibility with existing file processing workflows
|
446
|
+
while leveraging FAISS for efficient similarity search.
|
447
|
+
|
253
448
|
Args:
|
254
|
-
file (FileProcessorInterface):
|
449
|
+
file (FileProcessorInterface): File processor instance that implements getText()
|
450
|
+
|
451
|
+
Note:
|
452
|
+
Processed files are automatically available for context retrieval
|
453
|
+
in subsequent conversations without additional configuration.
|
255
454
|
"""
|
256
455
|
document = file.getText()
|
257
456
|
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
|
258
457
|
texts = text_splitter.split_documents(document)
|
259
458
|
|
260
459
|
if self.vector_store is None:
|
261
|
-
self.vector_store = FAISS.from_texts(
|
460
|
+
self.vector_store = FAISS.from_texts(
|
461
|
+
[doc.page_content for doc in texts],
|
462
|
+
self.embeddings
|
463
|
+
)
|
262
464
|
else:
|
263
465
|
self.vector_store.add_texts([doc.page_content for doc in texts])
|
264
466
|
|
467
|
+
def clear_memory(self):
|
468
|
+
"""
|
469
|
+
Clear conversation history and processed file context.
|
470
|
+
|
471
|
+
This method resets the bot to a clean state, removing all conversation
|
472
|
+
history and processed file context.
|
473
|
+
"""
|
474
|
+
self.chat_history.clear()
|
475
|
+
self.vector_store = None
|
476
|
+
|
265
477
|
def get_chat_history(self) -> List[BaseMessage]:
|
266
478
|
"""
|
267
|
-
|
479
|
+
Retrieve a copy of the current conversation history.
|
480
|
+
|
481
|
+
Returns:
|
482
|
+
List[BaseMessage]: Copy of the conversation history
|
483
|
+
"""
|
484
|
+
return self.chat_history.copy()
|
485
|
+
|
486
|
+
def set_chat_history(self, history: List[BaseMessage]):
|
487
|
+
"""
|
488
|
+
Set the conversation history from a list of BaseMessage instances.
|
489
|
+
|
490
|
+
Args:
|
491
|
+
history (List[BaseMessage]): New conversation history to set
|
492
|
+
"""
|
493
|
+
self.chat_history = history.copy()
|
268
494
|
|
495
|
+
def _get_context(self, query: str) -> str:
|
496
|
+
"""
|
497
|
+
Retrieve relevant context from processed files using similarity search.
|
498
|
+
|
499
|
+
This method performs semantic search over processed file content to find
|
500
|
+
the most relevant information for the current query.
|
501
|
+
|
502
|
+
Args:
|
503
|
+
query (str): The query to search for relevant context
|
504
|
+
|
269
505
|
Returns:
|
270
|
-
|
506
|
+
str: Concatenated relevant context from processed files
|
271
507
|
"""
|
508
|
+
if self.vector_store:
|
509
|
+
docs = self.vector_store.similarity_search(query, k=4)
|
510
|
+
return "\n".join([doc.page_content for doc in docs])
|
511
|
+
return ""
|
512
|
+
|
513
|
+
def process_file(self, file: FileProcessorInterface):
|
514
|
+
"""API original - Procesa archivo y lo añade al vector store"""
|
515
|
+
document = file.getText()
|
516
|
+
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
|
517
|
+
texts = text_splitter.split_documents(document)
|
518
|
+
|
519
|
+
if self.vector_store is None:
|
520
|
+
self.vector_store = FAISS.from_texts(
|
521
|
+
[doc.page_content for doc in texts],
|
522
|
+
self.embeddings
|
523
|
+
)
|
524
|
+
else:
|
525
|
+
self.vector_store.add_texts([doc.page_content for doc in texts])
|
526
|
+
|
527
|
+
def clear_memory(self):
|
528
|
+
"""API original - Limpia la memoria de conversación"""
|
529
|
+
self.chat_history.clear()
|
530
|
+
self.vector_store = None
|
531
|
+
|
532
|
+
def get_chat_history(self) -> List[BaseMessage]:
|
533
|
+
"""API original - Obtiene el historial completo"""
|
272
534
|
return self.chat_history.copy()
|
273
535
|
|
274
536
|
def set_chat_history(self, history: List[BaseMessage]):
|
537
|
+
"""API original - Establece el historial de conversación"""
|
538
|
+
self.chat_history = history.copy()
|
539
|
+
|
540
|
+
def _get_context(self, query: str) -> str:
|
541
|
+
"""Obtiene contexto relevante de archivos procesados"""
|
542
|
+
if self.vector_store:
|
543
|
+
docs = self.vector_store.similarity_search(query, k=4)
|
544
|
+
return "\n".join([doc.page_content for doc in docs])
|
545
|
+
return ""
|
546
|
+
|
547
|
+
# ===== MODERN ENHANCED CAPABILITIES =====
|
548
|
+
|
549
|
+
def get_response_with_thread(self, user_input: str, thread_id: str) -> ResponseModel:
|
275
550
|
"""
|
276
|
-
|
551
|
+
Generate response with automatic conversation persistence using thread IDs.
|
552
|
+
|
553
|
+
This method leverages LangGraph's checkpointing system to automatically
|
554
|
+
persist and retrieve conversation state based on thread identifiers.
|
555
|
+
|
556
|
+
Args:
|
557
|
+
user_input (str): The user's message or query
|
558
|
+
thread_id (str): Unique identifier for the conversation thread
|
559
|
+
|
560
|
+
Returns:
|
561
|
+
ResponseModel: Structured response with token usage and content
|
562
|
+
|
563
|
+
Raises:
|
564
|
+
ValueError: If checkpointer is not configured during initialization
|
565
|
+
|
566
|
+
Note:
|
567
|
+
Each thread_id maintains independent conversation state, enabling
|
568
|
+
multiple concurrent conversations per user or session.
|
569
|
+
"""
|
570
|
+
if not self.checkpointer:
|
571
|
+
raise ValueError("Checkpointer not configured. Initialize with use_checkpointer=True")
|
572
|
+
|
573
|
+
config = {"configurable": {"thread_id": thread_id}}
|
574
|
+
|
575
|
+
initial_state = {
|
576
|
+
"messages": [HumanMessage(content=user_input)],
|
577
|
+
"context": ""
|
578
|
+
}
|
579
|
+
|
580
|
+
result = self.graph.invoke(initial_state, config=config)
|
581
|
+
|
582
|
+
# Extract final response
|
583
|
+
final_response = ""
|
584
|
+
for msg in reversed(result["messages"]):
|
585
|
+
if isinstance(msg, AIMessage) and msg.content:
|
586
|
+
final_response = msg.content
|
587
|
+
break
|
588
|
+
|
589
|
+
# Extract token usage
|
590
|
+
token_usage = {}
|
591
|
+
last_message = result["messages"][-1]
|
592
|
+
if hasattr(last_message, 'response_metadata'):
|
593
|
+
token_usage = last_message.response_metadata.get('token_usage', {})
|
594
|
+
|
595
|
+
return ResponseModel(
|
596
|
+
user_tokens=token_usage.get('prompt_tokens', 0),
|
597
|
+
bot_tokens=token_usage.get('completion_tokens', 0),
|
598
|
+
response=final_response
|
599
|
+
)
|
600
|
+
|
601
|
+
def stream_with_thread(self, user_input: str, thread_id: str) -> Generator[Dict[str, Any], None, None]:
|
602
|
+
"""
|
603
|
+
Stream response with automatic conversation persistence.
|
604
|
+
|
605
|
+
This method combines streaming capabilities with thread-based persistence,
|
606
|
+
allowing real-time response generation while maintaining conversation state.
|
607
|
+
|
608
|
+
Args:
|
609
|
+
user_input (str): The user's message or query
|
610
|
+
thread_id (str): Unique identifier for the conversation thread
|
611
|
+
|
612
|
+
Yields:
|
613
|
+
Dict[str, Any]: Workflow execution chunks containing intermediate states
|
614
|
+
|
615
|
+
Raises:
|
616
|
+
ValueError: If checkpointer is not configured during initialization
|
617
|
+
"""
|
618
|
+
if not self.checkpointer:
|
619
|
+
raise ValueError("Checkpointer not configured. Initialize with use_checkpointer=True")
|
620
|
+
|
621
|
+
config = {"configurable": {"thread_id": thread_id}}
|
622
|
+
|
623
|
+
initial_state = {
|
624
|
+
"messages": [HumanMessage(content=user_input)],
|
625
|
+
"context": ""
|
626
|
+
}
|
627
|
+
|
628
|
+
for chunk in self.graph.stream(initial_state, config=config):
|
629
|
+
yield chunk
|
277
630
|
|
631
|
+
def get_mcp_status(self) -> Dict[str, Any]:
|
632
|
+
"""
|
633
|
+
Retrieve the current status of MCP (Model Context Protocol) integration.
|
634
|
+
|
635
|
+
This method provides diagnostic information about MCP server connections
|
636
|
+
and tool availability for monitoring and debugging purposes.
|
637
|
+
|
638
|
+
Returns:
|
639
|
+
Dict[str, Any]: MCP status information containing:
|
640
|
+
- mcp_enabled: Whether MCP is active
|
641
|
+
- servers: List of connected server names
|
642
|
+
- tools_count: Number of MCP-sourced tools
|
643
|
+
- total_tools: Total number of available tools
|
644
|
+
"""
|
645
|
+
if not self.mcp_client:
|
646
|
+
return {"mcp_enabled": False, "servers": [], "tools_count": 0}
|
647
|
+
|
648
|
+
mcp_tools_count = len([
|
649
|
+
tool for tool in self.tools
|
650
|
+
if hasattr(tool, '__module__') and tool.__module__ and 'mcp' in tool.__module__
|
651
|
+
])
|
652
|
+
|
653
|
+
return {
|
654
|
+
"mcp_enabled": True,
|
655
|
+
"servers": list(getattr(self.mcp_client, '_servers', {}).keys()),
|
656
|
+
"tools_count": mcp_tools_count,
|
657
|
+
"total_tools": len(self.tools)
|
658
|
+
}
|
659
|
+
|
660
|
+
def add_tool_dynamically(self, tool: BaseTool):
|
661
|
+
"""
|
662
|
+
Add a tool to the bot's capabilities at runtime.
|
663
|
+
|
664
|
+
This method allows dynamic tool addition after initialization, automatically
|
665
|
+
updating the model binding and workflow configuration.
|
666
|
+
|
278
667
|
Args:
|
279
|
-
|
668
|
+
tool (BaseTool): The LangChain tool to add to the bot's capabilities
|
669
|
+
|
670
|
+
Note:
|
671
|
+
Adding tools dynamically triggers a complete workflow reconstruction
|
672
|
+
to ensure proper tool integration and binding.
|
280
673
|
"""
|
281
|
-
self.
|
674
|
+
self.tools.append(tool)
|
675
|
+
# Reconstruct model binding and workflow with new tool
|
676
|
+
self.model_with_tools = self._prepare_model_with_tools()
|
677
|
+
self.instructions = self._build_modern_instructions()
|
678
|
+
self.graph = self._create_modern_workflow()
|
679
|
+
|
680
|
+
# ===== UTILITY AND DIAGNOSTIC METHODS =====
|
681
|
+
|
682
|
+
def get_workflow_state(self) -> Dict[str, Any]:
|
683
|
+
"""
|
684
|
+
Get current workflow configuration for debugging and monitoring.
|
685
|
+
|
686
|
+
Returns:
|
687
|
+
Dict[str, Any]: Workflow state information including:
|
688
|
+
- tools_count: Number of available tools
|
689
|
+
- has_checkpointer: Whether persistence is enabled
|
690
|
+
- has_vector_store: Whether file processing is active
|
691
|
+
- chat_history_length: Current conversation length
|
692
|
+
"""
|
693
|
+
return {
|
694
|
+
"tools_count": len(self.tools),
|
695
|
+
"has_checkpointer": self.checkpointer is not None,
|
696
|
+
"has_vector_store": self.vector_store is not None,
|
697
|
+
"chat_history_length": len(self.chat_history),
|
698
|
+
"mcp_enabled": self.mcp_client is not None
|
699
|
+
}
|
700
|
+
|
701
|
+
def reset_conversation(self):
|
702
|
+
"""
|
703
|
+
Reset conversation state while preserving configuration and processed files.
|
704
|
+
|
705
|
+
This method clears only the conversation history while maintaining
|
706
|
+
tool configurations, file context, and other persistent settings.
|
707
|
+
"""
|
708
|
+
self.chat_history.clear()
|
709
|
+
|
710
|
+
def get_tool_names(self) -> List[str]:
|
711
|
+
"""
|
712
|
+
Get list of available tool names for diagnostic purposes.
|
713
|
+
|
714
|
+
Returns:
|
715
|
+
List[str]: Names of all currently available tools
|
716
|
+
"""
|
717
|
+
return [tool.name for tool in self.tools]
|
718
|
+
|
719
|
+
# ===== FIN DE LA CLASE =====
|
720
|
+
# No hay métodos legacy innecesarios
|
721
|
+
|
722
|
+
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: sonika-langchain-bot
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.12
|
4
4
|
Summary: Agente langchain con LLM
|
5
5
|
Author: Erley Blanco Carvajal
|
6
6
|
License: MIT License
|
@@ -10,7 +10,7 @@ Classifier: Operating System :: OS Independent
|
|
10
10
|
Requires-Python: >=3.6
|
11
11
|
Description-Content-Type: text/markdown
|
12
12
|
License-File: LICENSE
|
13
|
-
Requires-Dist: langchain==0.
|
13
|
+
Requires-Dist: langchain-mcp-adapters==0.1.9
|
14
14
|
Requires-Dist: langchain-community==0.3.26
|
15
15
|
Requires-Dist: langchain-core==0.3.66
|
16
16
|
Requires-Dist: langchain-openai==0.3.24
|
@@ -1,14 +1,14 @@
|
|
1
1
|
sonika_langchain_bot/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
2
|
sonika_langchain_bot/langchain_bdi.py,sha256=ithc55azP5XSPb8AGRUrDGYnVI6I4IqpqElLNat4BAQ,7024
|
3
|
-
sonika_langchain_bot/langchain_bot_agent.py,sha256=
|
3
|
+
sonika_langchain_bot/langchain_bot_agent.py,sha256=SBqiLWWTpSHi_v_pC6XelHyMpiSC-g2n1fGipZbgUQk,28631
|
4
4
|
sonika_langchain_bot/langchain_bot_agent_bdi.py,sha256=Ev0hhRQYe6kyGAHiFDhFsfu6QnTwUFaA9oB8DfNV7u4,8613
|
5
5
|
sonika_langchain_bot/langchain_clasificator.py,sha256=GR85ZAliymBSoDa5PXB31BvJkuiokGjS2v3RLdXnzzk,1381
|
6
6
|
sonika_langchain_bot/langchain_class.py,sha256=5anB6v_wCzEoAJRb8fV9lPPS72E7-k51y_aeiip8RAw,1114
|
7
7
|
sonika_langchain_bot/langchain_files.py,sha256=SEyqnJgBc_nbCIG31eypunBbO33T5AHFOhQZcghTks4,381
|
8
8
|
sonika_langchain_bot/langchain_models.py,sha256=vqSSZ48tNofrTMLv1QugDdyey2MuIeSdlLSD37AnzkI,2235
|
9
9
|
sonika_langchain_bot/langchain_tools.py,sha256=y7wLf1DbUua3QIvz938Ek-JIMOuQhrOIptJadW8OIsU,466
|
10
|
-
sonika_langchain_bot-0.0.
|
11
|
-
sonika_langchain_bot-0.0.
|
12
|
-
sonika_langchain_bot-0.0.
|
13
|
-
sonika_langchain_bot-0.0.
|
14
|
-
sonika_langchain_bot-0.0.
|
10
|
+
sonika_langchain_bot-0.0.12.dist-info/licenses/LICENSE,sha256=O8VZ4aU_rUMAArvYTm2bshcZ991huv_tpfB5BKHH9Q8,1064
|
11
|
+
sonika_langchain_bot-0.0.12.dist-info/METADATA,sha256=hgBZ7RuN4683itsMD6gggfJrOtg-IrqV5tNbcIgnWb0,6380
|
12
|
+
sonika_langchain_bot-0.0.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
13
|
+
sonika_langchain_bot-0.0.12.dist-info/top_level.txt,sha256=UsTTSZFEw2wrPSVh4ufu01e2m_E7O_QVYT_k4zCQaAE,21
|
14
|
+
sonika_langchain_bot-0.0.12.dist-info/RECORD,,
|
File without changes
|
{sonika_langchain_bot-0.0.11.dist-info → sonika_langchain_bot-0.0.12.dist-info}/licenses/LICENSE
RENAMED
File without changes
|
{sonika_langchain_bot-0.0.11.dist-info → sonika_langchain_bot-0.0.12.dist-info}/top_level.txt
RENAMED
File without changes
|