sonika-langchain-bot 0.0.10__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.
@@ -0,0 +1,722 @@
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
+ """
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
+
152
+ Returns:
153
+ str: Complete system instructions including tool descriptions
154
+ """
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
168
+
169
+ def _create_modern_workflow(self) -> StateGraph:
170
+ """
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
+
179
+ Returns:
180
+ StateGraph: Compiled LangGraph workflow ready for execution
181
+ """
182
+
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"
274
+
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
+ }
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()
311
+
312
+ # ===== LEGACY API COMPATIBILITY =====
313
+
314
+ def get_response(self, user_input: str) -> ResponseModel:
315
+ """
316
+ Generate a response while maintaining 100% API compatibility.
317
+
318
+ This method provides the primary interface for single-turn conversations,
319
+ maintaining backward compatibility with existing ChatService implementations.
320
+
321
+ Args:
322
+ user_input (str): The user's message or query
323
+
324
+ Returns:
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.
333
+ """
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]:
370
+ """
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.
385
+ """
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
+ ])
409
+
410
+ def load_conversation_history(self, messages: List[Message]):
411
+ """
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
+
417
+ Args:
418
+ messages (List[Message]): List of Django Message model instances
419
+ Expected to have 'content' and 'is_bot' attributes
420
+ """
421
+ self.chat_history.clear()
422
+ for message in messages:
423
+ if message.is_bot:
424
+ self.chat_history.append(AIMessage(content=message.content))
425
+ else:
426
+ self.chat_history.append(HumanMessage(content=message.content))
427
+
428
+ def save_messages(self, user_message: str, bot_response: str):
429
+ """
430
+ Save messages to internal conversation history.
431
+
432
+ This method provides backward compatibility for manual history management.
433
+
434
+ Args:
435
+ user_message (str): The user's input message
436
+ bot_response (str): The bot's generated response
437
+ """
438
+ self.chat_history.append(HumanMessage(content=user_message))
439
+ self.chat_history.append(AIMessage(content=bot_response))
440
+
441
+ def process_file(self, file: FileProcessorInterface):
442
+ """
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
+
448
+ Args:
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.
454
+ """
455
+ document = file.getText()
456
+ text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
457
+ texts = text_splitter.split_documents(document)
458
+
459
+ if self.vector_store is None:
460
+ self.vector_store = FAISS.from_texts(
461
+ [doc.page_content for doc in texts],
462
+ self.embeddings
463
+ )
464
+ else:
465
+ self.vector_store.add_texts([doc.page_content for doc in texts])
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
+
477
+ def get_chat_history(self) -> List[BaseMessage]:
478
+ """
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()
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
+
505
+ Returns:
506
+ str: Concatenated relevant context from processed files
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"""
534
+ return self.chat_history.copy()
535
+
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:
550
+ """
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
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
+
667
+ Args:
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.
673
+ """
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
+
@@ -0,0 +1,215 @@
1
+ Metadata-Version: 2.4
2
+ Name: sonika-langchain-bot
3
+ Version: 0.0.12
4
+ Summary: Agente langchain con LLM
5
+ Author: Erley Blanco Carvajal
6
+ License: MIT License
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.6
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: langchain-mcp-adapters==0.1.9
14
+ Requires-Dist: langchain-community==0.3.26
15
+ Requires-Dist: langchain-core==0.3.66
16
+ Requires-Dist: langchain-openai==0.3.24
17
+ Requires-Dist: langgraph==0.4.8
18
+ Requires-Dist: langgraph-checkpoint==2.1.0
19
+ Requires-Dist: langgraph-sdk==0.1.70
20
+ Requires-Dist: dataclasses-json==0.6.7
21
+ Requires-Dist: python-dateutil==2.9.0.post0
22
+ Requires-Dist: pydantic==2.11.7
23
+ Requires-Dist: faiss-cpu==1.11.0
24
+ Requires-Dist: pypdf==5.6.1
25
+ Requires-Dist: python-dotenv==1.0.1
26
+ Requires-Dist: typing_extensions==4.14.0
27
+ Requires-Dist: typing-inspect==0.9.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: sphinx<9.0.0,>=8.1.3; extra == "dev"
30
+ Requires-Dist: sphinx-rtd-theme<4.0.0,>=3.0.1; extra == "dev"
31
+ Dynamic: author
32
+ Dynamic: classifier
33
+ Dynamic: description
34
+ Dynamic: description-content-type
35
+ Dynamic: license
36
+ Dynamic: license-file
37
+ Dynamic: provides-extra
38
+ Dynamic: requires-dist
39
+ Dynamic: requires-python
40
+ Dynamic: summary
41
+
42
+ # Sonika LangChain Bot <a href="https://pepy.tech/projects/sonika-langchain-bot"><img src="https://static.pepy.tech/badge/sonika-langchain-bot" alt="PyPI Downloads"></a>
43
+
44
+ A Python library that implements a conversational agent using LangChain with tool execution capabilities and text classification.
45
+
46
+ ## Installation
47
+
48
+ ```bash
49
+ pip install sonika-langchain-bot
50
+ ```
51
+
52
+ ## Prerequisites
53
+
54
+ You'll need the following API keys:
55
+
56
+ - OpenAI API Key
57
+
58
+ Create a `.env` file in the root of your project with the following variables:
59
+
60
+ ```env
61
+ OPENAI_API_KEY=your_api_key_here
62
+ ```
63
+
64
+ ## Key Features
65
+
66
+ - Conversational agent with tool execution capabilities
67
+ - Text classification with structured output
68
+ - Custom tool integration
69
+ - Streaming responses
70
+ - Conversation history management
71
+ - Flexible instruction-based behavior
72
+
73
+ ## Basic Usage
74
+
75
+ ### Agent with Tools Example
76
+
77
+ ```python
78
+ import os
79
+ from dotenv import load_dotenv
80
+ from langchain_openai import OpenAIEmbeddings
81
+ from sonika_langchain_bot.langchain_tools import EmailTool
82
+ from sonika_langchain_bot.langchain_bot_agent import LangChainBot
83
+ from sonika_langchain_bot.langchain_class import Message, ResponseModel
84
+ from sonika_langchain_bot.langchain_models import OpenAILanguageModel
85
+
86
+ # Load environment variables
87
+ load_dotenv()
88
+
89
+ # Get API key from .env file
90
+ api_key = os.getenv("OPENAI_API_KEY")
91
+
92
+ # Initialize language model and embeddings
93
+ language_model = OpenAILanguageModel(api_key, model_name='gpt-4o-mini-2024-07-18', temperature=1)
94
+ embeddings = OpenAIEmbeddings(api_key=api_key)
95
+
96
+ # Configure tools
97
+ tools = [EmailTool()]
98
+
99
+ # Create agent instance
100
+ bot = LangChainBot(language_model, embeddings, instructions="You are an agent", tools=tools)
101
+
102
+ # Load conversation history
103
+ bot.load_conversation_history([Message(content="My name is Erley", is_bot=False)])
104
+
105
+ # Get response
106
+ user_message = 'Send an email with the tool to erley@gmail.com with subject Hello and message Hello Erley'
107
+ response_model: ResponseModel = bot.get_response(user_message)
108
+
109
+ print(response_model)
110
+ ```
111
+
112
+ ### Streaming Response Example
113
+
114
+ ```python
115
+ import os
116
+ from dotenv import load_dotenv
117
+ from langchain_openai import OpenAIEmbeddings
118
+ from sonika_langchain_bot.langchain_bot_agent import LangChainBot
119
+ from sonika_langchain_bot.langchain_class import Message
120
+ from sonika_langchain_bot.langchain_models import OpenAILanguageModel
121
+
122
+ # Load environment variables
123
+ load_dotenv()
124
+
125
+ # Get API key from .env file
126
+ api_key = os.getenv("OPENAI_API_KEY")
127
+
128
+ # Initialize language model and embeddings
129
+ language_model = OpenAILanguageModel(api_key, model_name='gpt-4o-mini-2024-07-18', temperature=1)
130
+ embeddings = OpenAIEmbeddings(api_key=api_key)
131
+
132
+ # Create agent instance
133
+ bot = LangChainBot(language_model, embeddings, instructions="Only answers in english", tools=[])
134
+
135
+ # Load conversation history
136
+ bot.load_conversation_history([Message(content="My name is Erley", is_bot=False)])
137
+
138
+ # Get streaming response
139
+ user_message = 'Hello, what is my name?'
140
+ for chunk in bot.get_response_stream(user_message):
141
+ print(chunk)
142
+ ```
143
+
144
+ ### Text Classification Example
145
+
146
+ ```python
147
+ import os
148
+ from dotenv import load_dotenv
149
+ from sonika_langchain_bot.langchain_clasificator import TextClassifier
150
+ from sonika_langchain_bot.langchain_models import OpenAILanguageModel
151
+ from pydantic import BaseModel, Field
152
+
153
+ # Load environment variables
154
+ load_dotenv()
155
+
156
+ # Define classification structure with Pydantic
157
+ class Classification(BaseModel):
158
+ intention: str = Field()
159
+ sentiment: str = Field(..., enum=["happy", "neutral", "sad", "excited"])
160
+ aggressiveness: int = Field(
161
+ ...,
162
+ description="describes how aggressive the statement is, the higher the number the more aggressive",
163
+ enum=[1, 2, 3, 4, 5],
164
+ )
165
+ language: str = Field(
166
+ ..., enum=["spanish", "english", "french", "german", "italian"]
167
+ )
168
+
169
+ # Initialize classifier
170
+ api_key = os.getenv("OPENAI_API_KEY")
171
+ model = OpenAILanguageModel(api_key=api_key)
172
+ classifier = TextClassifier(llm=model, validation_class=Classification)
173
+
174
+ # Classify text
175
+ result = classifier.classify("how are you?")
176
+ print(result)
177
+ ```
178
+
179
+ ## Available Classes and Components
180
+
181
+ ### Core Classes
182
+
183
+ - **LangChainBot**: Main conversational agent for task execution with tools
184
+ - **OpenAILanguageModel**: Wrapper for OpenAI language models
185
+ - **TextClassifier**: Text classification using structured output
186
+ - **Message**: Message structure for conversation history
187
+ - **ResponseModel**: Response structure from agent interactions
188
+
189
+ ### Tools
190
+
191
+ - **EmailTool**: Tool for sending emails through the agent
192
+
193
+ ## Project Structure
194
+
195
+ ```
196
+ your_project/
197
+ ├── .env # Environment variables
198
+ ├── src/
199
+ │ └── sonika_langchain_bot/
200
+ │ ├── langchain_bot_agent.py
201
+ │ ├── langchain_clasificator.py
202
+ │ ├── langchain_class.py
203
+ │ ├── langchain_models.py
204
+ │ └── langchain_tools.py
205
+ └── tests/
206
+ └── test_bot.py
207
+ ```
208
+
209
+ ## Contributing
210
+
211
+ Contributions are welcome. Please open an issue to discuss major changes you'd like to make.
212
+
213
+ ## License
214
+
215
+ This project is licensed under the MIT License.
@@ -1,13 +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=SBqiLWWTpSHi_v_pC6XelHyMpiSC-g2n1fGipZbgUQk,28631
3
4
  sonika_langchain_bot/langchain_bot_agent_bdi.py,sha256=Ev0hhRQYe6kyGAHiFDhFsfu6QnTwUFaA9oB8DfNV7u4,8613
4
5
  sonika_langchain_bot/langchain_clasificator.py,sha256=GR85ZAliymBSoDa5PXB31BvJkuiokGjS2v3RLdXnzzk,1381
5
6
  sonika_langchain_bot/langchain_class.py,sha256=5anB6v_wCzEoAJRb8fV9lPPS72E7-k51y_aeiip8RAw,1114
6
7
  sonika_langchain_bot/langchain_files.py,sha256=SEyqnJgBc_nbCIG31eypunBbO33T5AHFOhQZcghTks4,381
7
8
  sonika_langchain_bot/langchain_models.py,sha256=vqSSZ48tNofrTMLv1QugDdyey2MuIeSdlLSD37AnzkI,2235
8
9
  sonika_langchain_bot/langchain_tools.py,sha256=y7wLf1DbUua3QIvz938Ek-JIMOuQhrOIptJadW8OIsU,466
9
- sonika_langchain_bot-0.0.10.dist-info/LICENSE,sha256=O8VZ4aU_rUMAArvYTm2bshcZ991huv_tpfB5BKHH9Q8,1064
10
- sonika_langchain_bot-0.0.10.dist-info/METADATA,sha256=UFs95OjxiI17n839Ylb2ou-eR8sJ11MrMu7V3TOa0sA,4455
11
- sonika_langchain_bot-0.0.10.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
12
- sonika_langchain_bot-0.0.10.dist-info/top_level.txt,sha256=UsTTSZFEw2wrPSVh4ufu01e2m_E7O_QVYT_k4zCQaAE,21
13
- sonika_langchain_bot-0.0.10.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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.5.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,145 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: sonika-langchain-bot
3
- Version: 0.0.10
4
- Summary: Agente langchain con LLM
5
- Author: Erley Blanco Carvajal
6
- License: MIT License
7
- Classifier: Programming Language :: Python :: 3
8
- Classifier: License :: OSI Approved :: MIT License
9
- Classifier: Operating System :: OS Independent
10
- Requires-Python: >=3.6
11
- Description-Content-Type: text/markdown
12
- License-File: LICENSE
13
- Requires-Dist: langchain <1.0.0,>=0.3.0
14
- Requires-Dist: langchain-community <1.0.0,>=0.3.0
15
- Requires-Dist: langchain-core <1.0.0,>=0.3.5
16
- Requires-Dist: langchain-openai <1.0.0,>=0.2.0
17
- Requires-Dist: langgraph <1.0.0,>=0.2.39
18
- Requires-Dist: langgraph-checkpoint <3.0.0,>=2.0.2
19
- Requires-Dist: langgraph-sdk <2.0.0,>=0.1.34
20
- Requires-Dist: dataclasses-json <1.0.0,>=0.6.7
21
- Requires-Dist: python-dateutil <3.0.0,>=2.9.0
22
- Requires-Dist: tiktoken <1.0.0,>=0.7.0
23
- Requires-Dist: pydantic <3.0.0,>=2.9.2
24
- Requires-Dist: faiss-cpu <2.0.0,>=1.8.0
25
- Requires-Dist: pypdf <6.0.0,>=5.0.0
26
- Requires-Dist: python-dotenv <2.0.0,>=1.0.1
27
- Requires-Dist: typing-extensions <5.0.0,>=4.12.0
28
- Requires-Dist: typing-inspect <1.0.0,>=0.9.0
29
- Provides-Extra: dev
30
- Requires-Dist: sphinx <9.0.0,>=8.1.3 ; extra == 'dev'
31
- Requires-Dist: sphinx-rtd-theme <4.0.0,>=3.0.1 ; extra == 'dev'
32
-
33
- # Sonika LangChain Bot <a href="https://pepy.tech/projects/sonika-langchain-bot"><img src="https://static.pepy.tech/badge/sonika-langchain-bot" alt="PyPI Downloads"></a>
34
-
35
-
36
- Una librería Python que implementa un bot conversacional utilizando LangChain con capacidades BDI (Belief-Desire-Intention) y clasificación de texto.
37
-
38
- ## Instalación
39
-
40
- ```bash
41
- pip install sonika-langchain-bot
42
- ```
43
-
44
- ## Requisitos previos
45
-
46
- Necesitarás las siguientes API keys:
47
-
48
- - OpenAI API Key
49
-
50
- Crea un archivo `.env` en la raíz de tu proyecto con las siguientes variables:
51
-
52
- ```env
53
- OPENAI_API_KEY=tu_api_key_aqui
54
- ```
55
-
56
- ## Características principales
57
-
58
- - Bot conversacional con arquitectura BDI
59
- - Clasificación de texto
60
- - Ejecución de código personalizado por medio de tools
61
-
62
- ## Uso básico
63
-
64
- ### Ejemplo de Bot BDI
65
-
66
- ```python
67
- from sonika_langchain_bot.langchain_bdi import Belief, BeliefType
68
- from sonika_langchain_bot.langchain_bot_agent_bdi import LangChainBot
69
- from sonika_langchain_bot.langchain_models import OpenAILanguageModel
70
- from langchain_openai import OpenAIEmbeddings
71
-
72
- # Inicializar el modelo de lenguaje
73
- language_model = OpenAILanguageModel(api_key, model_name='gpt-4o-mini', temperature=1)
74
- embeddings = OpenAIEmbeddings(api_key=api_key)
75
-
76
- # Configurar herramientas propias o de terceros
77
- search = TavilySearchResults(max_results=2, api_key=api_key_tavily)
78
- tools = [search]
79
-
80
- # Configurar creencias
81
- beliefs = [
82
- Belief(
83
- content="Eres un asistente de chat",
84
- type=BeliefType.PERSONALITY,
85
- confidence=1,
86
- source='personality'
87
- )
88
- ]
89
-
90
- # Crear instancia del bot
91
- bot = LangChainBot(language_model, embeddings, beliefs=beliefs, tools=tools)
92
-
93
- # Obtener respuesta
94
- response = bot.get_response("Hola como te llamas?")
95
-
96
- bot = LangChainBot(language_model, embeddings, beliefs=beliefs, tools=tools)
97
-
98
- user_message = 'Hola como me llamo?'
99
-
100
- #Cargas la conversacion previa con el bot
101
- bot.load_conversation_history([Message(content="Mi nombre es Erley", is_bot=False)])
102
- # Obtener la respuesta del bot
103
- response_model: ResponseModel = bot.get_response(user_message)
104
- bot_response = response_model
105
-
106
- print(bot_response)
107
-
108
- #o por streaming
109
- for chunk in bot.get_response_stream(user_message):
110
- print(chunk)
111
-
112
- ```
113
-
114
- ### Ejemplo de Clasificación de Texto
115
-
116
- ```python
117
- from sonika_langchain_bot.langchain_clasificator import TextClassifier
118
- from sonika_langchain_bot.langchain_models import OpenAILanguageModel
119
- from pydantic import BaseModel, Field
120
-
121
- # Definir estructura de clasificación
122
- class Classification(BaseModel):
123
- intention: str = Field()
124
- sentiment: str = Field(..., enum=["feliz", "neutral", "triste", "excitado"])
125
- aggressiveness: int = Field(
126
- ...,
127
- description="describes how aggressive the statement is",
128
- enum=[1, 2, 3, 4, 5],
129
- )
130
- language: str = Field(
131
- ..., enum=["español", "ingles", "frances", "aleman", "italiano"]
132
- )
133
-
134
- # Inicializar clasificador
135
- model = OpenAILanguageModel(api_key=api_key)
136
- classifier = TextClassifier(llm=model, validation_class=Classification)
137
-
138
- # Clasificar texto
139
- result = classifier.classify("Tu texto aquí")
140
- ```
141
-
142
- ## Contribución
143
-
144
- Las contribuciones son bienvenidas. Por favor, abre un issue para discutir los cambios importantes que te gustaría hacer.
145
-