sonika-langchain-bot 0.0.11__tar.gz → 0.0.13__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (19) hide show
  1. {sonika_langchain_bot-0.0.11/src/sonika_langchain_bot.egg-info → sonika_langchain_bot-0.0.13}/PKG-INFO +2 -2
  2. {sonika_langchain_bot-0.0.11 → sonika_langchain_bot-0.0.13}/setup.py +2 -2
  3. sonika_langchain_bot-0.0.13/src/sonika_langchain_bot/langchain_bot_agent.py +561 -0
  4. {sonika_langchain_bot-0.0.11 → sonika_langchain_bot-0.0.13/src/sonika_langchain_bot.egg-info}/PKG-INFO +2 -2
  5. {sonika_langchain_bot-0.0.11 → sonika_langchain_bot-0.0.13}/src/sonika_langchain_bot.egg-info/requires.txt +1 -1
  6. sonika_langchain_bot-0.0.11/src/sonika_langchain_bot/langchain_bot_agent.py +0 -281
  7. {sonika_langchain_bot-0.0.11 → sonika_langchain_bot-0.0.13}/LICENSE +0 -0
  8. {sonika_langchain_bot-0.0.11 → sonika_langchain_bot-0.0.13}/README.md +0 -0
  9. {sonika_langchain_bot-0.0.11 → sonika_langchain_bot-0.0.13}/setup.cfg +0 -0
  10. {sonika_langchain_bot-0.0.11 → sonika_langchain_bot-0.0.13}/src/sonika_langchain_bot/__init__.py +0 -0
  11. {sonika_langchain_bot-0.0.11 → sonika_langchain_bot-0.0.13}/src/sonika_langchain_bot/langchain_clasificator.py +0 -0
  12. {sonika_langchain_bot-0.0.11 → sonika_langchain_bot-0.0.13}/src/sonika_langchain_bot/langchain_class.py +0 -0
  13. {sonika_langchain_bot-0.0.11 → sonika_langchain_bot-0.0.13}/src/sonika_langchain_bot/langchain_files.py +0 -0
  14. {sonika_langchain_bot-0.0.11 → sonika_langchain_bot-0.0.13}/src/sonika_langchain_bot/langchain_models.py +0 -0
  15. {sonika_langchain_bot-0.0.11 → sonika_langchain_bot-0.0.13}/src/sonika_langchain_bot/langchain_tools.py +0 -0
  16. {sonika_langchain_bot-0.0.11 → sonika_langchain_bot-0.0.13}/src/sonika_langchain_bot.egg-info/SOURCES.txt +0 -0
  17. {sonika_langchain_bot-0.0.11 → sonika_langchain_bot-0.0.13}/src/sonika_langchain_bot.egg-info/dependency_links.txt +0 -0
  18. {sonika_langchain_bot-0.0.11 → sonika_langchain_bot-0.0.13}/src/sonika_langchain_bot.egg-info/top_level.txt +0 -0
  19. {sonika_langchain_bot-0.0.11 → sonika_langchain_bot-0.0.13}/test/test.py +0 -0
@@ -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.13
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
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="sonika-langchain-bot",
5
- version="0.0.11",
5
+ version="0.0.13",
6
6
  description="Agente langchain con LLM",
7
7
  author="Erley Blanco Carvajal",
8
8
  license="MIT License",
@@ -11,7 +11,7 @@ setup(
11
11
  packages=find_packages(where="src"), # Encuentra los paquetes dentro de "src"
12
12
  package_dir={"": "src"}, # Indica que los paquetes están en el directorio "src"
13
13
  install_requires=[
14
- "langchain==0.3.26",
14
+ "langchain-mcp-adapters==0.1.9",
15
15
  "langchain-community==0.3.26",
16
16
  "langchain-core==0.3.66",
17
17
  "langchain-openai==0.3.24",
@@ -0,0 +1,561 @@
1
+ from typing import Generator, List, Optional, Dict, Any, TypedDict, Annotated
2
+ import asyncio
3
+ from langchain.schema import AIMessage, HumanMessage, BaseMessage
4
+ from langchain_core.messages import ToolMessage
5
+ from langchain.text_splitter import CharacterTextSplitter
6
+ from langchain_community.vectorstores import FAISS
7
+ from langchain_community.tools import BaseTool
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
+
28
+
29
+ class LangChainBot:
30
+ """
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
43
+ """
44
+
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):
52
+ """
53
+ Initialize the modern LangGraph bot with optional MCP support.
54
+
55
+ Args:
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.
66
+ """
67
+ # Core components
68
+ self.language_model = language_model
69
+ self.embeddings = embeddings
70
+ self.base_instructions = instructions
71
+
72
+ # Backward compatibility attributes
73
+ self.chat_history: List[BaseMessage] = []
74
+ self.vector_store = None
75
+
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()
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
99
+
100
+ def _initialize_mcp(self, mcp_servers: Dict[str, Any]):
101
+ """
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.
120
+ """
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):
131
+ """
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
+
137
+ Returns:
138
+ The language model with tools bound, or the original model if no tools are available
139
+ """
140
+ if self.tools:
141
+ return self.language_model.model.bind_tools(self.tools)
142
+ return self.language_model.model
143
+
144
+ def _build_modern_instructions(self) -> str:
145
+ instructions = self.base_instructions
146
+
147
+ if self.tools:
148
+ tools_description = "\n\n# Available Tools\n\n"
149
+
150
+ for tool in self.tools:
151
+ tools_description += f"## {tool.name}\n"
152
+ tools_description += f"**Description:** {tool.description}\n\n"
153
+
154
+ # Opción 1: Tool con args_schema explícito (tu HTTPTool)
155
+ if hasattr(tool, 'args_schema') and tool.args_schema:
156
+ if hasattr(tool.args_schema, '__fields__'):
157
+ tools_description += f"**Parameters:**\n"
158
+ for field_name, field_info in tool.args_schema.__fields__.items():
159
+ required = "**REQUIRED**" if field_info.is_required() else "*optional*"
160
+ tools_description += f"- `{field_name}` ({field_info.annotation.__name__}, {required}): {field_info.description}\n"
161
+
162
+ # Opción 2: Tool básico sin args_schema (EmailTool)
163
+ elif hasattr(tool, '_run'):
164
+ tools_description += f"**Parameters:**\n"
165
+ import inspect
166
+ sig = inspect.signature(tool._run)
167
+ for param_name, param in sig.parameters.items():
168
+ if param_name != 'self':
169
+ param_type = param.annotation.__name__ if param.annotation != inspect.Parameter.empty else 'any'
170
+ required = "*optional*" if param.default != inspect.Parameter.empty else "**REQUIRED**"
171
+ default_info = f" (default: {param.default})" if param.default != inspect.Parameter.empty else ""
172
+ tools_description += f"- `{param_name}` ({param_type}, {required}){default_info}\n"
173
+
174
+ tools_description += "\n"
175
+
176
+ tools_description += ("## Usage Instructions\n"
177
+ "- Use the standard function calling format\n"
178
+ "- **MUST** provide all REQUIRED parameters\n"
179
+ "- Do NOT call tools with empty arguments\n")
180
+
181
+ instructions += tools_description
182
+
183
+ return instructions
184
+
185
+ def _create_modern_workflow(self) -> StateGraph:
186
+ """
187
+ Create a modern LangGraph workflow using idiomatic patterns.
188
+
189
+ This method constructs a state-based workflow that handles:
190
+ - Agent reasoning and response generation
191
+ - Automatic tool execution via ToolNode
192
+ - Context integration from processed files
193
+ - Error handling and fallback responses
194
+
195
+ Returns:
196
+ StateGraph: Compiled LangGraph workflow ready for execution
197
+ """
198
+
199
+ def agent_node(state: ChatState) -> ChatState:
200
+ """
201
+ Main agent node responsible for generating responses and initiating tool calls.
202
+
203
+ This node:
204
+ 1. Extracts the latest user message from the conversation state
205
+ 2. Retrieves relevant context from processed files
206
+ 3. Constructs a complete message history for the model
207
+ 4. Invokes the model with tool binding for native function calling
208
+ 5. Returns updated state with the model's response
209
+
210
+ Args:
211
+ state (ChatState): Current conversation state
212
+
213
+ Returns:
214
+ ChatState: Updated state with agent response
215
+ """
216
+ # Extract the most recent user message
217
+ last_user_message = None
218
+ for msg in reversed(state["messages"]):
219
+ if isinstance(msg, HumanMessage):
220
+ last_user_message = msg.content
221
+ break
222
+
223
+ if not last_user_message:
224
+ return state
225
+
226
+ # Retrieve contextual information from processed files
227
+ context = self._get_context(last_user_message)
228
+
229
+ # Build system prompt with optional context
230
+ system_content = self.instructions
231
+ if context:
232
+ system_content += f"\n\nContext from uploaded files:\n{context}"
233
+
234
+ # Construct message history in OpenAI format
235
+ messages = [{"role": "system", "content": system_content}]
236
+
237
+ # Add conversation history with simplified message handling
238
+ for msg in state["messages"]:
239
+ if isinstance(msg, HumanMessage):
240
+ messages.append({"role": "user", "content": msg.content})
241
+ elif isinstance(msg, AIMessage):
242
+ messages.append({"role": "assistant", "content": msg.content or ""})
243
+ elif isinstance(msg, ToolMessage):
244
+ # Convert tool results to user messages for context
245
+ messages.append({"role": "user", "content": f"Tool result: {msg.content}"})
246
+
247
+ try:
248
+ # Invoke model with native tool binding
249
+ response = self.model_with_tools.invoke(messages)
250
+
251
+ # Return updated state
252
+ return {
253
+ **state,
254
+ "context": context,
255
+ "messages": [response] # add_messages annotation handles proper appending
256
+ }
257
+
258
+ except Exception as e:
259
+ print(f"Error in agent_node: {e}")
260
+ # Graceful fallback for error scenarios
261
+ fallback_response = AIMessage(content="I apologize, but I encountered an error processing your request.")
262
+ return {
263
+ **state,
264
+ "context": context,
265
+ "messages": [fallback_response]
266
+ }
267
+
268
+ def should_continue(state: ChatState) -> str:
269
+ """
270
+ Conditional edge function to determine workflow continuation.
271
+
272
+ Analyzes the last message to decide whether to execute tools or end the workflow.
273
+ This leverages LangGraph's native tool calling detection.
274
+
275
+ Args:
276
+ state (ChatState): Current conversation state
277
+
278
+ Returns:
279
+ str: Next node to execute ("tools" or "end")
280
+ """
281
+ last_message = state["messages"][-1]
282
+
283
+ # Check for pending tool calls using native tool calling detection
284
+ if (isinstance(last_message, AIMessage) and
285
+ hasattr(last_message, 'tool_calls') and
286
+ last_message.tool_calls):
287
+ return "tools"
288
+
289
+ return "end"
290
+
291
+ # Construct the workflow graph
292
+ workflow = StateGraph(ChatState)
293
+
294
+ # Add primary agent node
295
+ workflow.add_node("agent", agent_node)
296
+
297
+ # Add tool execution node if tools are available
298
+ if self.tools:
299
+ # ToolNode automatically handles tool execution and result formatting
300
+ tool_node = ToolNode(self.tools)
301
+ workflow.add_node("tools", tool_node)
302
+
303
+ # Define workflow edges and entry point
304
+ workflow.set_entry_point("agent")
305
+
306
+ if self.tools:
307
+ # Conditional routing based on tool call presence
308
+ workflow.add_conditional_edges(
309
+ "agent",
310
+ should_continue,
311
+ {
312
+ "tools": "tools",
313
+ "end": END
314
+ }
315
+ )
316
+ # Return to agent after tool execution for final response formatting
317
+ workflow.add_edge("tools", "agent")
318
+ else:
319
+ # Direct termination if no tools are available
320
+ workflow.add_edge("agent", END)
321
+
322
+ # Compile workflow with optional checkpointing
323
+ if self.checkpointer:
324
+ return workflow.compile(checkpointer=self.checkpointer)
325
+ else:
326
+ return workflow.compile()
327
+
328
+ # ===== LEGACY API COMPATIBILITY =====
329
+
330
+ def get_response(self, user_input: str) -> ResponseModel:
331
+ """
332
+ Generate a response while maintaining 100% API compatibility.
333
+
334
+ This method provides the primary interface for single-turn conversations,
335
+ maintaining backward compatibility with existing ChatService implementations.
336
+
337
+ Args:
338
+ user_input (str): The user's message or query
339
+
340
+ Returns:
341
+ ResponseModel: Structured response containing:
342
+ - user_tokens: Input token count
343
+ - bot_tokens: Output token count
344
+ - response: Generated response text
345
+
346
+ Note:
347
+ This method automatically handles tool execution and context integration
348
+ from processed files while maintaining the original API signature.
349
+ """
350
+ # Prepare initial workflow state
351
+ initial_state = {
352
+ "messages": self.chat_history + [HumanMessage(content=user_input)],
353
+ "context": ""
354
+ }
355
+
356
+ # Execute the LangGraph workflow
357
+ result = self.graph.invoke(initial_state)
358
+
359
+ # Update internal conversation history
360
+ self.chat_history = result["messages"]
361
+
362
+ # Extract final response from the last assistant message
363
+ final_response = ""
364
+ total_input_tokens = 0
365
+ total_output_tokens = 0
366
+
367
+ for msg in reversed(result["messages"]):
368
+ if isinstance(msg, AIMessage) and msg.content:
369
+ final_response = msg.content
370
+ break
371
+
372
+ # Extract token usage from response metadata
373
+ last_message = result["messages"][-1]
374
+ if hasattr(last_message, 'response_metadata'):
375
+ token_usage = last_message.response_metadata.get('token_usage', {})
376
+ total_input_tokens = token_usage.get('prompt_tokens', 0)
377
+ total_output_tokens = token_usage.get('completion_tokens', 0)
378
+
379
+ return ResponseModel(
380
+ user_tokens=total_input_tokens,
381
+ bot_tokens=total_output_tokens,
382
+ response=final_response
383
+ )
384
+
385
+ def get_response_stream(self, user_input: str) -> Generator[str, None, None]:
386
+ """
387
+ Generate a streaming response for real-time user interaction.
388
+
389
+ This method provides streaming capabilities while maintaining backward
390
+ compatibility with the original API.
391
+
392
+ Args:
393
+ user_input (str): The user's message or query
394
+
395
+ Yields:
396
+ str: Response chunks as they are generated
397
+
398
+ Note:
399
+ Current implementation streams complete responses. For token-level
400
+ streaming, consider using the model's native streaming capabilities.
401
+ """
402
+ initial_state = {
403
+ "messages": self.chat_history + [HumanMessage(content=user_input)],
404
+ "context": ""
405
+ }
406
+
407
+ accumulated_response = ""
408
+
409
+ # Stream workflow execution
410
+ for chunk in self.graph.stream(initial_state):
411
+ # Extract content from workflow chunks
412
+ if "agent" in chunk:
413
+ for message in chunk["agent"]["messages"]:
414
+ if isinstance(message, AIMessage) and message.content:
415
+ # Stream complete responses (can be enhanced for token-level streaming)
416
+ accumulated_response = message.content
417
+ yield message.content
418
+
419
+ # Update conversation history after streaming completion
420
+ if accumulated_response:
421
+ self.chat_history.extend([
422
+ HumanMessage(content=user_input),
423
+ AIMessage(content=accumulated_response)
424
+ ])
425
+
426
+ def load_conversation_history(self, messages: List[Message]):
427
+ """
428
+ Load conversation history from Django model instances.
429
+
430
+ This method maintains compatibility with existing Django-based conversation
431
+ storage while preparing the history for modern LangGraph processing.
432
+
433
+ Args:
434
+ messages (List[Message]): List of Django Message model instances
435
+ Expected to have 'content' and 'is_bot' attributes
436
+ """
437
+ self.chat_history.clear()
438
+ for message in messages:
439
+ if message.is_bot:
440
+ self.chat_history.append(AIMessage(content=message.content))
441
+ else:
442
+ self.chat_history.append(HumanMessage(content=message.content))
443
+
444
+ def save_messages(self, user_message: str, bot_response: str):
445
+ """
446
+ Save messages to internal conversation history.
447
+
448
+ This method provides backward compatibility for manual history management.
449
+
450
+ Args:
451
+ user_message (str): The user's input message
452
+ bot_response (str): The bot's generated response
453
+ """
454
+ self.chat_history.append(HumanMessage(content=user_message))
455
+ self.chat_history.append(AIMessage(content=bot_response))
456
+
457
+ def process_file(self, file: FileProcessorInterface):
458
+ """
459
+ Process and index a file for contextual retrieval.
460
+
461
+ This method maintains compatibility with existing file processing workflows
462
+ while leveraging FAISS for efficient similarity search.
463
+
464
+ Args:
465
+ file (FileProcessorInterface): File processor instance that implements getText()
466
+
467
+ Note:
468
+ Processed files are automatically available for context retrieval
469
+ in subsequent conversations without additional configuration.
470
+ """
471
+ document = file.getText()
472
+ text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
473
+ texts = text_splitter.split_documents(document)
474
+
475
+ if self.vector_store is None:
476
+ self.vector_store = FAISS.from_texts(
477
+ [doc.page_content for doc in texts],
478
+ self.embeddings
479
+ )
480
+ else:
481
+ self.vector_store.add_texts([doc.page_content for doc in texts])
482
+
483
+ def clear_memory(self):
484
+ """
485
+ Clear conversation history and processed file context.
486
+
487
+ This method resets the bot to a clean state, removing all conversation
488
+ history and processed file context.
489
+ """
490
+ self.chat_history.clear()
491
+ self.vector_store = None
492
+
493
+ def get_chat_history(self) -> List[BaseMessage]:
494
+ """
495
+ Retrieve a copy of the current conversation history.
496
+
497
+ Returns:
498
+ List[BaseMessage]: Copy of the conversation history
499
+ """
500
+ return self.chat_history.copy()
501
+
502
+ def set_chat_history(self, history: List[BaseMessage]):
503
+ """
504
+ Set the conversation history from a list of BaseMessage instances.
505
+
506
+ Args:
507
+ history (List[BaseMessage]): New conversation history to set
508
+ """
509
+ self.chat_history = history.copy()
510
+
511
+ def _get_context(self, query: str) -> str:
512
+ """
513
+ Retrieve relevant context from processed files using similarity search.
514
+
515
+ This method performs semantic search over processed file content to find
516
+ the most relevant information for the current query.
517
+
518
+ Args:
519
+ query (str): The query to search for relevant context
520
+
521
+ Returns:
522
+ str: Concatenated relevant context from processed files
523
+ """
524
+ if self.vector_store:
525
+ docs = self.vector_store.similarity_search(query, k=4)
526
+ return "\n".join([doc.page_content for doc in docs])
527
+ return ""
528
+
529
+ def process_file(self, file: FileProcessorInterface):
530
+ """API original - Procesa archivo y lo añade al vector store"""
531
+ document = file.getText()
532
+ text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
533
+ texts = text_splitter.split_documents(document)
534
+
535
+ if self.vector_store is None:
536
+ self.vector_store = FAISS.from_texts(
537
+ [doc.page_content for doc in texts],
538
+ self.embeddings
539
+ )
540
+ else:
541
+ self.vector_store.add_texts([doc.page_content for doc in texts])
542
+
543
+ def clear_memory(self):
544
+ """API original - Limpia la memoria de conversación"""
545
+ self.chat_history.clear()
546
+ self.vector_store = None
547
+
548
+ def get_chat_history(self) -> List[BaseMessage]:
549
+ """API original - Obtiene el historial completo"""
550
+ return self.chat_history.copy()
551
+
552
+ def set_chat_history(self, history: List[BaseMessage]):
553
+ """API original - Establece el historial de conversación"""
554
+ self.chat_history = history.copy()
555
+
556
+ def _get_context(self, query: str) -> str:
557
+ """Obtiene contexto relevante de archivos procesados"""
558
+ if self.vector_store:
559
+ docs = self.vector_store.similarity_search(query, k=4)
560
+ return "\n".join([doc.page_content for doc in docs])
561
+ return ""
@@ -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.13
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,4 +1,4 @@
1
- langchain==0.3.26
1
+ langchain-mcp-adapters==0.1.9
2
2
  langchain-community==0.3.26
3
3
  langchain-core==0.3.66
4
4
  langchain-openai==0.3.24
@@ -1,281 +0,0 @@
1
- from typing import Generator, List
2
- from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder, SystemMessagePromptTemplate, HumanMessagePromptTemplate
3
- from langchain.schema import AIMessage, HumanMessage, BaseMessage
4
- from langchain.text_splitter import CharacterTextSplitter
5
- 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
- from langchain_community.tools import BaseTool
10
- import re
11
-
12
- class LangChainBot:
13
- """
14
- Implementación principal del bot conversacional con capacidades de procesamiento de archivos,
15
- memoria de conversación y uso de herramientas personalizadas.
16
- """
17
-
18
- def __init__(self, language_model: ILanguageModel, embeddings: IEmbeddings, instructions: str, tools: List[BaseTool]):
19
- """
20
- Inicializa el bot con el modelo de lenguaje, embeddings y herramientas necesarias.
21
-
22
- 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
27
- """
28
- self.language_model = language_model
29
- self.embeddings = embeddings
30
- # Reemplazamos ConversationBufferMemory con una lista simple de mensajes
31
- self.chat_history: List[BaseMessage] = []
32
- self.memory_agent = MemorySaver()
33
- 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
- # Agregar las instrucciones de herramientas a las instrucciones base
76
- self.instructions += tools_instructions
77
-
78
-
79
- def _create_conversation_chain(self):
80
- """
81
- Crea la cadena de conversación con el prompt template y la memoria.
82
- """
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):
95
- """
96
- Crea el ejecutor del agente con las herramientas configuradas.
97
-
98
- Returns:
99
- Agent: Agente configurado con las herramientas
100
- """
101
- return create_react_agent(self.language_model.model, self.tools, checkpointer=self.memory_agent)
102
-
103
- def _getInstruccionTool(self, bot_response):
104
- """
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
-
110
- Returns:
111
- str: Instrucción extraída o cadena vacía si no se encuentra
112
- """
113
- patron = r'\*\*\*(.*?)\*\*\*'
114
- coincidencia = re.search(patron, bot_response)
115
- return coincidencia.group(1).strip() if coincidencia else ''
116
-
117
- def get_response(self, user_input: str) -> ResponseModel:
118
- """
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
-
124
- Returns:
125
- ResponseModel: Modelo de respuesta con tokens y texto
126
- """
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
-
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}}
147
-
148
- result_stream = self.agent_executor.stream(
149
- {"messages": messages}, config
150
- )
151
-
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)
171
-
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
181
- """
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
- })
192
-
193
- full_response = ""
194
- for response in result_stream:
195
- content = response.content
196
- full_response += content
197
- yield content
198
-
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
- Args:
207
- query (str): Consulta para buscar contexto
208
-
209
- Returns:
210
- str: Contexto encontrado o cadena vacía
211
- """
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):
218
- """
219
- Limpia la memoria de conversación y el almacén de vectores.
220
- """
221
- self.chat_history.clear()
222
- self.vector_store = None
223
-
224
- def load_conversation_history(self, messages: List[Message]):
225
- """
226
- Carga el historial de conversación previo usando la estructura de mensajes simplificada.
227
-
228
- Args:
229
- messages: Lista de objetos Message que representan cada mensaje.
230
- """
231
- self.chat_history.clear()
232
- for message in messages:
233
- if message.is_bot:
234
- self.chat_history.append(AIMessage(content=message.content))
235
- else:
236
- self.chat_history.append(HumanMessage(content=message.content))
237
-
238
- def save_messages(self, user_message: str, bot_response: str):
239
- """
240
- Guarda los mensajes en el historial de conversación.
241
-
242
- Args:
243
- user_message (str): Mensaje del usuario
244
- bot_response (str): Respuesta del bot
245
- """
246
- self.chat_history.append(HumanMessage(content=user_message))
247
- self.chat_history.append(AIMessage(content=bot_response))
248
-
249
- def process_file(self, file: FileProcessorInterface):
250
- """
251
- Procesa un archivo y lo añade al almacén de vectores.
252
-
253
- Args:
254
- file (FileProcessorInterface): Archivo a procesar
255
- """
256
- document = file.getText()
257
- text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
258
- texts = text_splitter.split_documents(document)
259
-
260
- if self.vector_store is None:
261
- self.vector_store = FAISS.from_texts([doc.page_content for doc in texts], self.embeddings)
262
- else:
263
- self.vector_store.add_texts([doc.page_content for doc in texts])
264
-
265
- def get_chat_history(self) -> List[BaseMessage]:
266
- """
267
- Obtiene el historial completo de la conversación.
268
-
269
- Returns:
270
- List[BaseMessage]: Lista de mensajes de la conversación
271
- """
272
- return self.chat_history.copy()
273
-
274
- def set_chat_history(self, history: List[BaseMessage]):
275
- """
276
- Establece el historial de conversación.
277
-
278
- Args:
279
- history (List[BaseMessage]): Lista de mensajes a establecer
280
- """
281
- self.chat_history = history.copy()