sonika-langchain-bot 0.0.15__py3-none-any.whl → 0.0.20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sonika-langchain-bot might be problematic. Click here for more details.

@@ -1,10 +1,12 @@
1
- from typing import Generator, List, Optional, Dict, Any, TypedDict, Annotated
1
+ from typing import Generator, List, Optional, Dict, Any, TypedDict, Annotated, Callable
2
2
  import asyncio
3
+ import logging
3
4
  from langchain.schema import AIMessage, HumanMessage, BaseMessage
4
5
  from langchain_core.messages import ToolMessage
5
6
  from langchain.text_splitter import CharacterTextSplitter
6
7
  from langchain_community.vectorstores import FAISS
7
8
  from langchain_community.tools import BaseTool
9
+ from langchain.callbacks.base import BaseCallbackHandler
8
10
  from langgraph.graph import StateGraph, END, add_messages
9
11
  from langgraph.prebuilt import ToolNode
10
12
  from langgraph.checkpoint.memory import MemorySaver
@@ -24,6 +26,101 @@ class ChatState(TypedDict):
24
26
  messages: Annotated[List[BaseMessage], add_messages]
25
27
  context: str
26
28
 
29
+ class _InternalToolLogger(BaseCallbackHandler):
30
+ """
31
+ Internal callback handler that bridges LangChain callbacks to user-provided functions.
32
+
33
+ This class is used internally to forward tool execution events to the optional
34
+ callback functions provided by the user during bot initialization.
35
+ """
36
+
37
+ def __init__(self,
38
+ on_start: Optional[Callable[[str, str], None]] = None,
39
+ on_end: Optional[Callable[[str, str], None]] = None,
40
+ on_error: Optional[Callable[[str, str], None]] = None):
41
+ """
42
+ Initialize the internal tool logger.
43
+
44
+ Args:
45
+ on_start: Optional callback function called when a tool starts execution
46
+ on_end: Optional callback function called when a tool completes successfully
47
+ on_error: Optional callback function called when a tool encounters an error
48
+ """
49
+ super().__init__()
50
+ self.on_start_callback = on_start
51
+ self.on_end_callback = on_end
52
+ self.on_error_callback = on_error
53
+ self.current_tool_name = None
54
+ self.tool_executions = [] # Para tracking interno si se necesita
55
+
56
+ def on_tool_start(self, serialized: Dict[str, Any], input_str: str, **kwargs) -> None:
57
+
58
+ print(f"DEBUG: on_tool_start se ejecutó!") # ← AGREGAR ESTO
59
+
60
+
61
+ """Called when a tool starts executing."""
62
+ tool_name = serialized.get("name", "unknown")
63
+ self.current_tool_name = tool_name
64
+
65
+ # Track execution internally
66
+ self.tool_executions.append({
67
+ "tool": tool_name,
68
+ "input": input_str,
69
+ "status": "started"
70
+ })
71
+
72
+ # Call user's callback if provided
73
+ if self.on_start_callback:
74
+ try:
75
+ self.on_start_callback(tool_name, input_str)
76
+ except Exception as e:
77
+ # Don't let user callback errors break the workflow
78
+ logging.error(f"Error in on_tool_start callback: {e}")
79
+
80
+ def on_tool_end(self, output: str, **kwargs) -> None:
81
+ print(f"DEBUG: on_tool_end se ejecutó!")
82
+ tool_name = self.current_tool_name or "unknown"
83
+
84
+ # Convert output to string if it's a ToolMessage or other object
85
+ if hasattr(output, 'content'):
86
+ output_str = output.content
87
+ elif isinstance(output, str):
88
+ output_str = output
89
+ else:
90
+ output_str = str(output)
91
+
92
+ # Update internal tracking
93
+ if self.tool_executions:
94
+ self.tool_executions[-1]["status"] = "success"
95
+ self.tool_executions[-1]["output"] = output_str
96
+
97
+ # Call user's callback if provided
98
+ if self.on_end_callback:
99
+ try:
100
+ self.on_end_callback(tool_name, output_str)
101
+ except Exception as e:
102
+ logging.error(f"Error in on_tool_end callback: {e}")
103
+
104
+ self.current_tool_name = None
105
+
106
+ def on_tool_error(self, error: Exception, **kwargs) -> None: # ← CORRECTO
107
+ print(f"DEBUG: on_tool_error se ejecutó!")
108
+ tool_name = self.current_tool_name or "unknown"
109
+ error_message = str(error)
110
+
111
+ # Update internal tracking
112
+ if self.tool_executions:
113
+ self.tool_executions[-1]["status"] = "error"
114
+ self.tool_executions[-1]["error"] = error_message
115
+
116
+ # Call user's callback if provided
117
+ if self.on_error_callback:
118
+ try:
119
+ self.on_error_callback(tool_name, error_message)
120
+ except Exception as e:
121
+ logging.error(f"Error in on_tool_error callback: {e}")
122
+
123
+ self.current_tool_name = None
27
124
 
28
125
  class LangChainBot:
29
126
  """
@@ -38,6 +135,7 @@ class LangChainBot:
38
135
  - File processing with vector search
39
136
  - Thread-based conversation persistence
40
137
  - Streaming responses
138
+ - Tool execution callbacks for real-time monitoring
41
139
  - Backward compatibility with legacy APIs
42
140
  """
43
141
 
@@ -47,9 +145,13 @@ class LangChainBot:
47
145
  instructions: str,
48
146
  tools: Optional[List[BaseTool]] = None,
49
147
  mcp_servers: Optional[Dict[str, Any]] = None,
50
- use_checkpointer: bool = False):
148
+ use_checkpointer: bool = False,
149
+ logger: Optional[logging.Logger] = None,
150
+ on_tool_start: Optional[Callable[[str, str], None]] = None,
151
+ on_tool_end: Optional[Callable[[str, str], None]] = None,
152
+ on_tool_error: Optional[Callable[[str, str], None]] = None):
51
153
  """
52
- Initialize the modern LangGraph bot with optional MCP support.
154
+ Initialize the modern LangGraph bot with optional MCP support and tool execution callbacks.
53
155
 
54
156
  Args:
55
157
  language_model (ILanguageModel): The language model to use for generation
@@ -58,11 +160,36 @@ class LangChainBot:
58
160
  tools (List[BaseTool], optional): Traditional LangChain tools to bind to the model
59
161
  mcp_servers (Dict[str, Any], optional): MCP server configurations for dynamic tool loading
60
162
  use_checkpointer (bool): Enable automatic conversation persistence using LangGraph checkpoints
163
+ logger (Optional[logging.Logger]): Logger instance for error tracking (silent by default if not provided)
164
+ on_tool_start (Callable[[str, str], None], optional): Callback function executed when a tool starts.
165
+ Receives (tool_name: str, input_data: str)
166
+ on_tool_end (Callable[[str, str], None], optional): Callback function executed when a tool completes successfully.
167
+ Receives (tool_name: str, output: str)
168
+ on_tool_error (Callable[[str, str], None], optional): Callback function executed when a tool fails.
169
+ Receives (tool_name: str, error_message: str)
61
170
 
62
171
  Note:
63
172
  The instructions will be automatically enhanced with tool descriptions
64
173
  when tools are provided, eliminating the need for manual tool instruction formatting.
174
+
175
+ Example:
176
+ ```python
177
+ def on_tool_execution(tool_name: str, input_data: str):
178
+ print(f"Tool {tool_name} started with input: {input_data}")
179
+
180
+ bot = LangChainBot(
181
+ language_model=model,
182
+ embeddings=embeddings,
183
+ instructions="You are a helpful assistant",
184
+ on_tool_start=on_tool_execution
185
+ )
186
+ ```
65
187
  """
188
+ # Configure logger (silent by default if not provided)
189
+ self.logger = logger or logging.getLogger(__name__)
190
+ if logger is None:
191
+ self.logger.addHandler(logging.NullHandler())
192
+
66
193
  # Core components
67
194
  self.language_model = language_model
68
195
  self.embeddings = embeddings
@@ -76,6 +203,11 @@ class LangChainBot:
76
203
  self.tools = tools or []
77
204
  self.mcp_client = None
78
205
 
206
+ # Tool execution callbacks
207
+ self.on_tool_start = on_tool_start
208
+ self.on_tool_end = on_tool_end
209
+ self.on_tool_error = on_tool_error
210
+
79
211
  # Initialize MCP servers if provided
80
212
  if mcp_servers:
81
213
  self._initialize_mcp(mcp_servers)
@@ -121,9 +253,9 @@ class LangChainBot:
121
253
  self.mcp_client = MultiServerMCPClient(mcp_servers)
122
254
  mcp_tools = asyncio.run(self.mcp_client.get_tools())
123
255
  self.tools.extend(mcp_tools)
124
- print(f"✅ MCP initialized: {len(mcp_tools)} tools from {len(mcp_servers)} servers")
125
256
  except Exception as e:
126
- print(f"⚠️ MCP initialization error: {e}")
257
+ self.logger.error(f"Error inicializando MCP: {e}")
258
+ self.logger.exception("Traceback completo:")
127
259
  self.mcp_client = None
128
260
 
129
261
  def _prepare_model_with_tools(self):
@@ -264,7 +396,8 @@ class LangChainBot:
264
396
  }
265
397
 
266
398
  except Exception as e:
267
- print(f"Error in agent_node: {e}")
399
+ self.logger.error(f"Error en agent_node: {e}")
400
+ self.logger.exception("Traceback completo:")
268
401
  # Graceful fallback for error scenarios
269
402
  fallback_response = AIMessage(content="I apologize, but I encountered an error processing your request.")
270
403
  return {
@@ -341,6 +474,7 @@ class LangChainBot:
341
474
 
342
475
  This method provides the primary interface for single-turn conversations,
343
476
  maintaining backward compatibility with existing ChatService implementations.
477
+ Tool execution callbacks (if provided) will be triggered during execution.
344
478
 
345
479
  Args:
346
480
  user_input (str): The user's message or query
@@ -361,11 +495,18 @@ class LangChainBot:
361
495
  "context": ""
362
496
  }
363
497
 
364
- # Execute the LangGraph workflow
365
- #result = self.graph.invoke(initial_state)
366
-
367
- # Siempre usar ainvoke (funciona para ambos casos)
368
- result = asyncio.run(self.graph.ainvoke(initial_state))
498
+ # Create callback handler if any callbacks are provided
499
+ config = {}
500
+ if self.on_tool_start or self.on_tool_end or self.on_tool_error:
501
+ tool_logger = _InternalToolLogger(
502
+ on_start=self.on_tool_start,
503
+ on_end=self.on_tool_end,
504
+ on_error=self.on_tool_error
505
+ )
506
+ config["callbacks"] = [tool_logger]
507
+
508
+ # Execute the LangGraph workflow with callbacks
509
+ result = asyncio.run(self.graph.ainvoke(initial_state, config=config))
369
510
 
370
511
  # Update internal conversation history
371
512
  self.chat_history = result["messages"]
@@ -398,7 +539,8 @@ class LangChainBot:
398
539
  Generate a streaming response for real-time user interaction.
399
540
 
400
541
  This method provides streaming capabilities while maintaining backward
401
- compatibility with the original API.
542
+ compatibility with the original API. Tool execution callbacks (if provided)
543
+ will be triggered during execution.
402
544
 
403
545
  Args:
404
546
  user_input (str): The user's message or query
@@ -415,10 +557,20 @@ class LangChainBot:
415
557
  "context": ""
416
558
  }
417
559
 
560
+ # Create callback handler if any callbacks are provided
561
+ config = {}
562
+ if self.on_tool_start or self.on_tool_end or self.on_tool_error:
563
+ tool_logger = _InternalToolLogger(
564
+ on_start=self.on_tool_start,
565
+ on_end=self.on_tool_end,
566
+ on_error=self.on_tool_error
567
+ )
568
+ config["callbacks"] = [tool_logger]
569
+
418
570
  accumulated_response = ""
419
571
 
420
- # Stream workflow execution
421
- for chunk in self.graph.stream(initial_state):
572
+ # Stream workflow execution with callbacks
573
+ for chunk in self.graph.stream(initial_state, config=config):
422
574
  # Extract content from workflow chunks
423
575
  if "agent" in chunk:
424
576
  for message in chunk["agent"]["messages"]:
@@ -532,40 +684,6 @@ class LangChainBot:
532
684
  Returns:
533
685
  str: Concatenated relevant context from processed files
534
686
  """
535
- if self.vector_store:
536
- docs = self.vector_store.similarity_search(query, k=4)
537
- return "\n".join([doc.page_content for doc in docs])
538
- return ""
539
-
540
- def process_file(self, file: FileProcessorInterface):
541
- """API original - Procesa archivo y lo añade al vector store"""
542
- document = file.getText()
543
- text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
544
- texts = text_splitter.split_documents(document)
545
-
546
- if self.vector_store is None:
547
- self.vector_store = FAISS.from_texts(
548
- [doc.page_content for doc in texts],
549
- self.embeddings
550
- )
551
- else:
552
- self.vector_store.add_texts([doc.page_content for doc in texts])
553
-
554
- def clear_memory(self):
555
- """API original - Limpia la memoria de conversación"""
556
- self.chat_history.clear()
557
- self.vector_store = None
558
-
559
- def get_chat_history(self) -> List[BaseMessage]:
560
- """API original - Obtiene el historial completo"""
561
- return self.chat_history.copy()
562
-
563
- def set_chat_history(self, history: List[BaseMessage]):
564
- """API original - Establece el historial de conversación"""
565
- self.chat_history = history.copy()
566
-
567
- def _get_context(self, query: str) -> str:
568
- """Obtiene contexto relevante de archivos procesados"""
569
687
  if self.vector_store:
570
688
  docs = self.vector_store.similarity_search(query, k=4)
571
689
  return "\n".join([doc.page_content for doc in docs])
@@ -2,16 +2,30 @@ from pydantic import BaseModel
2
2
  from typing import Dict, Any, Type
3
3
  from sonika_langchain_bot.langchain_class import ILanguageModel
4
4
 
5
- # Clase para realizar la clasificación de texto
5
+ class ClassificationResponse(BaseModel):
6
+ """Respuesta de clasificación con tokens utilizados"""
7
+ input_tokens: int
8
+ output_tokens: int
9
+ result: Dict[str, Any]
10
+
6
11
  class TextClassifier:
7
12
  def __init__(self, validation_class: Type[BaseModel], llm: ILanguageModel):
8
- self.llm =llm
13
+ self.llm = llm
9
14
  self.validation_class = validation_class
10
- #configuramos el modelo para que tenga una estructura de salida
11
- self.llm.model = self.llm.model.with_structured_output(validation_class)
15
+ # Guardamos ambas versiones del modelo
16
+ self.original_model = self.llm.model # Sin structured output
17
+ self.structured_model = self.llm.model.with_structured_output(validation_class)
12
18
 
13
- def classify(self, text: str) -> Dict[str, Any]:
14
- # Crear el template del prompt
19
+ def classify(self, text: str) -> ClassificationResponse:
20
+ """
21
+ Clasifica el texto según la clase de validación.
22
+
23
+ Args:
24
+ text: Texto a clasificar
25
+
26
+ Returns:
27
+ ClassificationResponse: Objeto con result, input_tokens y output_tokens
28
+ """
15
29
  prompt = f"""
16
30
  Classify the following text based on the properties defined in the validation class.
17
31
 
@@ -19,12 +33,34 @@ class TextClassifier:
19
33
 
20
34
  Only extract the properties mentioned in the validation class.
21
35
  """
22
- response = self.llm.invoke(prompt=prompt)
23
36
 
24
- # Asegurarse de que el `response` es de la clase de validación proporcionada
37
+ # Primero invocamos el modelo ORIGINAL para obtener metadata de tokens
38
+ raw_response = self.original_model.invoke(prompt)
39
+
40
+ # Extraer información de tokens del AIMessage original
41
+ input_tokens = 0
42
+ output_tokens = 0
43
+
44
+ if hasattr(raw_response, 'response_metadata'):
45
+ token_usage = raw_response.response_metadata.get('token_usage', {})
46
+ input_tokens = token_usage.get('prompt_tokens', 0)
47
+ output_tokens = token_usage.get('completion_tokens', 0)
48
+
49
+ # Ahora invocamos con structured output para obtener el objeto parseado
50
+ response = self.structured_model.invoke(prompt)
51
+
52
+ # Validar que el response es de la clase correcta
25
53
  if isinstance(response, self.validation_class):
26
- # Crear el resultado dinámicamente basado en los atributos de la clase de validación
27
- result = {field: getattr(response, field) for field in self.validation_class.__fields__.keys()}
28
- return result
54
+ # Crear el resultado dinámicamente basado en los atributos
55
+ result_data = {
56
+ field: getattr(response, field)
57
+ for field in self.validation_class.__fields__.keys()
58
+ }
59
+
60
+ return ClassificationResponse(
61
+ input_tokens=input_tokens,
62
+ output_tokens=output_tokens,
63
+ result=result_data
64
+ )
29
65
  else:
30
- raise ValueError(f"The response is not of type '{self.validation_class.__name__}'")
66
+ raise ValueError(f"The response is not of type '{self.validation_class.__name__}'")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sonika-langchain-bot
3
- Version: 0.0.15
3
+ Version: 0.0.20
4
4
  Summary: Agente langchain con LLM
5
5
  Author: Erley Blanco Carvajal
6
6
  License: MIT License
@@ -1,15 +1,15 @@
1
1
  sonika_langchain_bot/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  sonika_langchain_bot/document_processor.py,sha256=RuHT22Zt-psoe4adFWKwBJ0gi638fq8r2S5WZoDK8fY,10979
3
3
  sonika_langchain_bot/langchain_bdi.py,sha256=ithc55azP5XSPb8AGRUrDGYnVI6I4IqpqElLNat4BAQ,7024
4
- sonika_langchain_bot/langchain_bot_agent.py,sha256=LlzrINl543dPwizkQ-tW47OWzud0sP18Uwb-ZhxMHeA,23968
4
+ sonika_langchain_bot/langchain_bot_agent.py,sha256=l1Kj4iDnGSH-1NZkFxdlVCKOQxoDMsPjWNdxS3GapcA,29214
5
5
  sonika_langchain_bot/langchain_bot_agent_bdi.py,sha256=Ev0hhRQYe6kyGAHiFDhFsfu6QnTwUFaA9oB8DfNV7u4,8613
6
- sonika_langchain_bot/langchain_clasificator.py,sha256=GR85ZAliymBSoDa5PXB31BvJkuiokGjS2v3RLdXnzzk,1381
6
+ sonika_langchain_bot/langchain_clasificator.py,sha256=h0-H_1bqgA04rF2ZHh5zOg2PinqTuLQMcSK7AGK4uw8,2583
7
7
  sonika_langchain_bot/langchain_class.py,sha256=5anB6v_wCzEoAJRb8fV9lPPS72E7-k51y_aeiip8RAw,1114
8
8
  sonika_langchain_bot/langchain_files.py,sha256=SEyqnJgBc_nbCIG31eypunBbO33T5AHFOhQZcghTks4,381
9
9
  sonika_langchain_bot/langchain_models.py,sha256=vqSSZ48tNofrTMLv1QugDdyey2MuIeSdlLSD37AnzkI,2235
10
10
  sonika_langchain_bot/langchain_tools.py,sha256=y7wLf1DbUua3QIvz938Ek-JIMOuQhrOIptJadW8OIsU,466
11
- sonika_langchain_bot-0.0.15.dist-info/licenses/LICENSE,sha256=O8VZ4aU_rUMAArvYTm2bshcZ991huv_tpfB5BKHH9Q8,1064
12
- sonika_langchain_bot-0.0.15.dist-info/METADATA,sha256=TkIrUOf7OyjqybcPfdxsJkIAr_uKYPeh3cY1oVe8f4w,6508
13
- sonika_langchain_bot-0.0.15.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- sonika_langchain_bot-0.0.15.dist-info/top_level.txt,sha256=UsTTSZFEw2wrPSVh4ufu01e2m_E7O_QVYT_k4zCQaAE,21
15
- sonika_langchain_bot-0.0.15.dist-info/RECORD,,
11
+ sonika_langchain_bot-0.0.20.dist-info/licenses/LICENSE,sha256=O8VZ4aU_rUMAArvYTm2bshcZ991huv_tpfB5BKHH9Q8,1064
12
+ sonika_langchain_bot-0.0.20.dist-info/METADATA,sha256=bIPx5NtqGhSIRI1nPP-PZInj8MUhHRheq7VRwQNqOXY,6508
13
+ sonika_langchain_bot-0.0.20.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ sonika_langchain_bot-0.0.20.dist-info/top_level.txt,sha256=UsTTSZFEw2wrPSVh4ufu01e2m_E7O_QVYT_k4zCQaAE,21
15
+ sonika_langchain_bot-0.0.20.dist-info/RECORD,,