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.
@@ -1,232 +1,422 @@
1
- from typing import Generator, List
2
- from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder, SystemMessagePromptTemplate, HumanMessagePromptTemplate
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 re
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
- Implementación principal del bot conversacional con capacidades de procesamiento de archivos,
15
- memoria de conversación y uso de herramientas personalizadas.
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, language_model: ILanguageModel, embeddings: IEmbeddings, instructions: str, tools: List[BaseTool]):
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
- Inicializa el bot con el modelo de lenguaje, embeddings y herramientas necesarias.
53
+ Initialize the modern LangGraph bot with optional MCP support.
21
54
 
22
55
  Args:
23
- language_model (ILanguageModel): Modelo de lenguaje a utilizar
24
- embeddings (IEmbeddings): Modelo de embeddings para procesamiento de texto
25
- instructions (str): Instrucciones del sistema
26
- tools (List[BaseTool]): Lista de herramientas disponibles
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
- # Reemplazamos ConversationBufferMemory con una lista simple de mensajes
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
- # Agregar las instrucciones de herramientas a las instrucciones base
76
- self.instructions += tools_instructions
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 _create_conversation_chain(self):
100
+ def _initialize_mcp(self, mcp_servers: Dict[str, Any]):
80
101
  """
81
- Crea la cadena de conversación con el prompt template y la memoria.
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
- full_system_prompt = f"{self.instructions}\n\n"
84
-
85
- prompt = ChatPromptTemplate.from_messages([
86
- SystemMessagePromptTemplate.from_template(full_system_prompt),
87
- MessagesPlaceholder(variable_name="history"),
88
- HumanMessagePromptTemplate.from_template("{input}")
89
- ])
90
-
91
- # Usando RunnableSequence para reemplazar LLMChain
92
- return prompt | self.language_model.model
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
- Crea el ejecutor del agente con las herramientas configuradas.
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
- Agent: Agente configurado con las herramientas
138
+ The language model with tools bound, or the original model if no tools are available
100
139
  """
101
- return create_react_agent(self.language_model.model, self.tools, checkpointer=self.memory_agent)
140
+ if self.tools:
141
+ return self.language_model.model.bind_tools(self.tools)
142
+ return self.language_model.model
102
143
 
103
- def _getInstruccionTool(self, bot_response):
144
+ def _build_modern_instructions(self) -> str:
104
145
  """
105
- Extrae las instrucciones para herramientas del texto de respuesta del bot.
106
-
107
- Args:
108
- bot_response (str): Respuesta del bot a analizar
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: Instrucción extraída o cadena vacía si no se encuentra
153
+ str: Complete system instructions including tool descriptions
112
154
  """
113
- patron = r'\*\*\*(.*?)\*\*\*'
114
- coincidencia = re.search(patron, bot_response)
115
- return coincidencia.group(1).strip() if coincidencia else ''
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 get_response(self, user_input: str) -> ResponseModel:
169
+ def _create_modern_workflow(self) -> StateGraph:
118
170
  """
119
- Genera una respuesta para la entrada del usuario, procesando el contexto y ejecutando herramientas si es necesario.
120
-
121
- Args:
122
- user_input (str): Entrada del usuario
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
- ResponseModel: Modelo de respuesta con tokens y texto
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
- instruction_tool = self._getInstruccionTool(bot_response_content)
142
-
143
- if instruction_tool:
144
- messages = [HumanMessage(content=instruction_tool)]
145
- thread_id = "abc123"
146
- config = {"configurable": {"thread_id": thread_id}}
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
- result_stream = self.agent_executor.stream(
149
- {"messages": messages}, config
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
- tool_response = ""
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 get_response_stream(self, user_input: str) -> Generator[str, None, None]:
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
- context = self._get_context(user_input)
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
- full_response = ""
194
- for response in result_stream:
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
- query (str): Consulta para buscar contexto
208
-
322
+ user_input (str): The user's message or query
323
+
209
324
  Returns:
210
- str: Contexto encontrado o cadena vacía
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
- if self.vector_store:
213
- docs = self.vector_store.similarity_search(query)
214
- return "\n".join([doc.page_content for doc in docs])
215
- return ""
216
-
217
- def clear_memory(self):
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
- Limpia la memoria de conversación y el almacén de vectores.
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
- self.chat_history.clear()
222
- self.vector_store = None
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
- Carga el historial de conversación previo usando la estructura de mensajes simplificada.
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: Lista de objetos Message que representan cada mensaje.
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
- Guarda los mensajes en el historial de conversación.
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): Mensaje del usuario
244
- bot_response (str): Respuesta del bot
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
- Procesa un archivo y lo añade al almacén de vectores.
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): Archivo a procesar
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([doc.page_content for doc in texts], self.embeddings)
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
- Obtiene el historial completo de la conversación.
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
- List[BaseMessage]: Lista de mensajes de la conversación
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
- Establece el historial de conversación.
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
- history (List[BaseMessage]): Lista de mensajes a establecer
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.chat_history = history.copy()
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.11
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.3.26
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=z7jamAwZqQJEt7jWFZo2nMGWPS4dgTCghpKQdkR3BT4,10890
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.dist-info/licenses/LICENSE,sha256=O8VZ4aU_rUMAArvYTm2bshcZ991huv_tpfB5BKHH9Q8,1064
11
- sonika_langchain_bot-0.0.11.dist-info/METADATA,sha256=QBEkIYZbUqtflj8Lx-VBfTAY-3l6u4Mht0Dr1u1K3C8,6368
12
- sonika_langchain_bot-0.0.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
- sonika_langchain_bot-0.0.11.dist-info/top_level.txt,sha256=UsTTSZFEw2wrPSVh4ufu01e2m_E7O_QVYT_k4zCQaAE,21
14
- sonika_langchain_bot-0.0.11.dist-info/RECORD,,
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,,