phichat 0.1.0__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.
@@ -0,0 +1,34 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*" # Se activa al subir tags como v0.1.0
7
+
8
+ jobs:
9
+ build-and-publish:
10
+ name: Build and publish PhiChat to PyPI
11
+ runs-on: ubuntu-latest
12
+ environment: pypi
13
+ permissions:
14
+ id-token: write # Requerido para Trusted Publishing (OIDC)
15
+ contents: read
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Set up Python
21
+ uses: actions/setup-python@v5
22
+ with:
23
+ python-version: "3.11"
24
+
25
+ - name: Install uv
26
+ run: |
27
+ curl -LsSf https://astral.sh/uv/install.sh | sh
28
+ echo "$HOME/.local/bin" >> $GITHUB_PATH
29
+
30
+ - name: Build package
31
+ run: uv build
32
+
33
+ - name: Publish package
34
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,64 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .lib/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ share/python-wheels/
20
+ *.egg-info/
21
+ .installed.cfg
22
+ *.egg
23
+ MANIFEST
24
+
25
+ # Virtual Environments
26
+ .venv/
27
+ venv/
28
+ ENV/
29
+ env/
30
+ bin/
31
+ include/
32
+ lib/
33
+ share/
34
+ pyvenv.cfg
35
+
36
+ # Testing & Linting
37
+ .pytest_cache/
38
+ .tox/
39
+ .nox/
40
+ .coverage
41
+ .cache
42
+ nosetests.xml
43
+ coverage.xml
44
+ *.cover
45
+ .hypothesis/
46
+ .ruff_cache/
47
+ .mypy_cache/
48
+
49
+ # IDEs
50
+ .vscode/
51
+ .idea/
52
+ *.swp
53
+ *.swo
54
+
55
+ # Environment variables
56
+ .env
57
+ .env.local
58
+ .env.*.local
59
+
60
+ # OS
61
+ .DS_Store
62
+ Thumbs.db
63
+ laboratory/
64
+ uv.lock
@@ -0,0 +1,341 @@
1
+ # ChatPhi: Integración con flujos LangChain
2
+
3
+ ChatPhi hereda de `BaseChatModel` y es compatible con cualquier componente del ecosistema
4
+ LangChain. Este documento describe cómo integrarlo en los patrones más comunes, optimizado para **Phi-4**.
5
+
6
+ ---
7
+
8
+ ## Estructura del paquete
9
+
10
+ ```
11
+ PhiChat/
12
+ ├── __init__.py # Exporta ChatPhi, create_tool, run_tool_loop y helpers
13
+ ├── model.py # Clase ChatPhi (ChatOllama + fixes phi4)
14
+ ├── parsers.py # parse_phi4_tool_calls, inject_tool_system_message
15
+ ├── tools.py # create_tool, run_tool_loop, arun_tool_loop
16
+ └── constants/
17
+ ├── __init__.py # Exporta las constantes del subpaquete
18
+ └── constants.py # _PHI4_TOOL_SYSTEM_SUFFIX, _TOOL_CALL_PATTERNS
19
+ ```
20
+
21
+ ---
22
+
23
+ | Problema en Phi-4 | Solución en ChatPhi |
24
+ | --------------------------------------------- | ------------------------------------------------------------------------------------ |
25
+ | Ollama da Error 400 al usar `tools` nativo | `bind_tools` realiza un bypass e inyecta los schemas manualmente en el prompt |
26
+ | El modelo olvida sus propias llamadas pasadas | `_patch_messages` re-inyecta bloques `<|tool_call|>` en el historial |
27
+ | Las respuestas de herramientas son ignoradas | Se encapsulan en etiquetas `<|tool_response|>` nativas |
28
+ | Los tokens de control ensucian el stream | El filtro de streaming robusto bloquea prefijos fragmentados (ej. `<|tool`) |
29
+ | `with_structured_output` impreciso | Override personalizado que usa `format="json"` de Ollama con inyección de esquema |
30
+
31
+ > **Punto Crítico**: La descripción de la tool es lo único que Phi-4 lee para decidir si usarla. Debe ser clara, en lenguaje natural, y mencionar **cuándo** usarla, no solo qué hace.
32
+
33
+ ---
34
+
35
+ ## Importación
36
+
37
+ ```python
38
+ # Importación directa del paquete (recomendado)
39
+ from PhiChat import ChatPhi, create_tool, run_tool_loop
40
+
41
+ # O por módulo específico
42
+ from PhiChat.model import ChatPhi
43
+ from PhiChat.tools import create_tool, run_tool_loop
44
+ from PhiChat.parsers import parse_phi4_tool_calls
45
+ ```
46
+
47
+ ---
48
+
49
+ ## 1. Pipe básico (LCEL)
50
+
51
+ El patrón más simple. `ChatPhi` se usa como cualquier `ChatModel` en una chain.
52
+
53
+ ```python
54
+ from langchain_core.output_parsers import StrOutputParser
55
+ from langchain_core.prompts import ChatPromptTemplate
56
+ from PhiChat import ChatPhi
57
+
58
+ prompt = ChatPromptTemplate.from_messages([
59
+ ("system", "Eres un experto en {tema}."),
60
+ ("human", "{pregunta}"),
61
+ ])
62
+
63
+ chain = prompt | ChatPhi() | StrOutputParser()
64
+
65
+ result = chain.invoke({
66
+ "tema": "física cuántica",
67
+ "pregunta": "Explica el entrelazamiento cuántico en 2 líneas.",
68
+ })
69
+ ```
70
+
71
+ ---
72
+
73
+ ## 2. Tool calling con bind_tools
74
+
75
+ ### Definir tools con @tool
76
+
77
+ La descripción de cada tool es crítica. Phi-4 decide si usarla basándose exclusivamente en ese texto.
78
+
79
+ ```python
80
+ from langchain_core.tools import tool
81
+ from langchain_core.messages import HumanMessage
82
+ from PhiChat import ChatPhi
83
+
84
+ @tool
85
+ def convertir_moneda(monto: float, de: str, a: str) -> str:
86
+ """
87
+ Convierte un monto entre divisas.
88
+ Úsala cuando el usuario pregunte sobre conversiones de moneda o tipos de cambio.
89
+ """
90
+ tasas = {"USD": 1.0, "MXN": 17.2, "EUR": 0.92}
91
+ resultado = monto * (tasas[a] / tasas[de])
92
+ return f"{monto} {de} = {resultado:.2f} {a}"
93
+
94
+ llm = ChatPhi(temperature=0.3)
95
+ chain = llm.bind_tools([convertir_moneda])
96
+
97
+ result = chain.invoke([HumanMessage(content="¿Cuánto es 500 MXN en EUR?")])
98
+ # result.tool_calls -> [{"name": "convertir_moneda", "args": {...}, "id": "..."}]
99
+ ```
100
+
101
+ ### Definir tools con args_schema (Recomendado)
102
+
103
+ Phi-4 genera argumentos más precisos cuando el esquema tiene `Field(description=...)` explícito en cada campo.
104
+
105
+ ```python
106
+ from pydantic import BaseModel, Field
107
+ from PhiChat import ChatPhi, create_tool
108
+
109
+ class BusquedaInput(BaseModel):
110
+ query: str = Field(description="Término a buscar en la base de datos")
111
+ limite: int = Field(default=5, description="Número máximo de resultados a retornar")
112
+
113
+ def buscar_productos(query: str, limite: int = 5) -> str:
114
+ return f"Resultados para '{query}' (max {limite})"
115
+
116
+ tool_busqueda = create_tool(
117
+ func=buscar_productos,
118
+ description=(
119
+ "Busca productos en el catálogo por nombre o categoría. "
120
+ "Úsala cuando el usuario pregunte por disponibilidad o precios."
121
+ ),
122
+ args_schema=BusquedaInput,
123
+ )
124
+
125
+ chain = ChatPhi().bind_tools([tool_busqueda])
126
+ ```
127
+
128
+ ---
129
+
130
+ ## 3. Mejores Prácticas de Ingeniería (Verificadas)
131
+
132
+ Para garantizar una librería de calidad profesional, sigue estos patrones validados en LangChain v0.3:
133
+
134
+ ### A. Manejo de Errores y Auto-Corrección
135
+ No permitas que las herramientas lancen excepciones que rompan el flujo. Captura el error y devuélvelo como un mensaje para que Phi-4 pueda razonar y corregir su llamada.
136
+
137
+ ```python
138
+ @tool
139
+ def mi_herramienta_robusta(param: int) -> str:
140
+ """Ejecuta una acción sensible."""
141
+ try:
142
+ # Lógica de la herramienta
143
+ return "Éxito"
144
+ except Exception as e:
145
+ # Devolver el error al modelo para que intente otra estrategia
146
+ return f"Error en la herramienta: {str(e)}. Intenta con un valor diferente."
147
+ ```
148
+
149
+ ### B. Validación de Esquemas con Pydantic
150
+ Usa validadores de Pydantic para capturar errores de tipo antes de que lleguen a la lógica de negocio.
151
+
152
+ ```python
153
+ from pydantic import BaseModel, Field, field_validator
154
+
155
+ class CalculoInput(BaseModel):
156
+ valor: int = Field(description="Un número positivo")
157
+
158
+ @field_validator("valor")
159
+ @classmethod
160
+ def mayor_que_cero(cls, v: int) -> int:
161
+ if v <= 0:
162
+ raise ValueError("El valor debe ser mayor que cero")
163
+ return v
164
+ ```
165
+
166
+ ### C. Idempotencia y Transacciones Atómicas
167
+ Diseña herramientas que realicen operaciones atómicas. Si un agente reintenta una llamada debido a un timeout, la herramienta debe ser capaz de manejarlo sin duplicar efectos secundarios.
168
+
169
+ ---
170
+
171
+ ## 4. Orquestación con LangGraph: Conceptos Avanzados
172
+
173
+ ### A. Super-steps y Tolerancia a Fallos
174
+ LangGraph organiza la ejecución en "super-steps" (ticks del grafo).
175
+ - **Checkpoints**: Se guarda un `StateSnapshot` al final de cada super-step.
176
+ - **Task-level Writes**: Si un nodo falla dentro de un super-step paralelo, LangGraph guarda los resultados de los nodos que sí terminaron. Al reanudar, **solo se re-ejecutan los nodos fallidos**, optimizando el uso de tokens y tiempo.
177
+
178
+ ### B. Estructura de un StateSnapshot
179
+ Cuando llamas a `agent.get_state(config)`, obtienes un objeto con:
180
+ - `values`: Valores actuales del estado (mensajes, variables).
181
+ - `next`: Tupla con los nombres de los nodos que se ejecutarán a continuación (vacía si el grafo terminó).
182
+ - `metadata`: Incluye el `source` (input, loop, update) y el `step` (contador de super-steps).
183
+ - `parent_config`: Referencia al checkpoint anterior, permitiendo navegar hacia atrás.
184
+
185
+ ### C. Time Travel y Replay
186
+ Puedes re-ejecutar el grafo desde cualquier punto pasado utilizando un `checkpoint_id`:
187
+
188
+ ```python
189
+ # Buscar un checkpoint específico en el historial
190
+ history = list(agent.get_state_history(config))
191
+ old_checkpoint = history[5] # Por ejemplo, 5 pasos atrás
192
+
193
+ # Reanudar desde ese punto
194
+ agent.invoke(None, old_checkpoint.config)
195
+ ```
196
+
197
+ ---
198
+
199
+ ## 5. Mejores Prácticas para la Interfaz de Usuario (UI)
200
+ Si estás construyendo un chat basado en `PhiChat`, sigue estas guías para renderizar herramientas:
201
+
202
+ 1. **Estados del Ciclo de Vida**: Maneja siempre los tres estados: `pending` (ejecutando), `completed` (éxito) y `error` (fallo).
203
+ 2. **Streaming Reactivo**: Usa el `tool_call_chunks` emitido por `ChatPhi` para mostrar un "Loading Card" instantáneo antes de que la herramienta termine de ejecutarse.
204
+ 3. **Parseo Seguro**: Los resultados de las herramientas llegan como strings. Usa bloques `try/except` al parsear JSON en el frontend y ofrece una vista genérica (ej. JSON colapsable) si no tienes un componente visual específico para esa herramienta.
205
+
206
+ ---
207
+
208
+ ## 6. Salida estructurada
209
+
210
+ Override del método base. Usa `format="json"` de Ollama internamente e inyecta el esquema en el system prompt de forma automática.
211
+
212
+ ```python
213
+ from pydantic import BaseModel, Field
214
+ from langchain_core.messages import HumanMessage
215
+ from PhiChat import ChatPhi
216
+
217
+ class Resumen(BaseModel):
218
+ titulo: str = Field(description="Título del texto en max. 5 palabras")
219
+ ideas_clave: list[str] = Field(description="Lista de 3 ideas principales")
220
+ sentimiento: str = Field(description="Positivo, Negativo o Neutro")
221
+ puntaje: float = Field(description="Puntaje de 0.0 a 10.0")
222
+
223
+ llm = ChatPhi().with_structured_output(Resumen)
224
+
225
+ result = llm.invoke([HumanMessage(content="Analiza este texto: ...")])
226
+ # result -> Resumen(titulo="...", ideas_clave=[...], ...)
227
+ ```
228
+
229
+ ---
230
+
231
+ ## 4. Loop agéntico sin AgentExecutor
232
+
233
+ Útil para ejecutar múltiples llamadas a herramientas de forma secuencial sin la sobrecarga de un motor de agentes completo.
234
+
235
+ ```python
236
+ from langchain_core.messages import SystemMessage, HumanMessage
237
+ from langchain_core.tools import tool
238
+ from PhiChat import ChatPhi
239
+
240
+ @tool
241
+ def buscar_producto(nombre: str) -> str:
242
+ """Busca productos en el catálogo."""
243
+ return f"Detalles de {nombre}..."
244
+
245
+ llm = ChatPhi(temperature=0.2)
246
+ messages = [HumanMessage(content="¿Tienen laptop?")]
247
+
248
+ # Loop: LLM -> tool_call -> resultado -> LLM -> respuesta final
249
+ result = llm.run_tool_loop(
250
+ messages=messages,
251
+ tools=[buscar_producto],
252
+ verbose=True
253
+ )
254
+ ```
255
+
256
+ > **Nota**: Para aplicaciones asíncronas (web/servidores), utiliza `await llm.arun_tool_loop(...)`.
257
+
258
+ ---
259
+
260
+ ## 5. Streaming
261
+
262
+ El streaming filtra automáticamente los tokens de control y emite chunks limpios.
263
+
264
+ ```python
265
+ llm = ChatPhi()
266
+
267
+ for chunk in llm.stream("Explica la relatividad"):
268
+ print(chunk.content, end="", flush=True)
269
+ ```
270
+
271
+ ---
272
+
273
+ ## 6. Integración con LangGraph (Recomendado)
274
+
275
+ PhiChat es totalmente compatible con `create_react_agent` y persistencia de estado.
276
+
277
+ ```python
278
+ from langgraph.prebuilt import create_react_agent
279
+ from langgraph.checkpoint.memory import MemorySaver
280
+ from PhiChat import ChatPhi
281
+
282
+ llm = ChatPhi(temperature=0)
283
+ memory = MemorySaver()
284
+ agent = create_react_agent(llm, tools=[mi_tool], checkpointer=memory)
285
+
286
+ config = {"configurable": {"thread_id": "1"}}
287
+ result = agent.invoke({"messages": [("user", "Hola")]}, config)
288
+
289
+ # Inspección y Manipulación del Estado
290
+ # Recupera el estado actual de la conversación
291
+ state = agent.get_state(config)
292
+ print(state.values["messages"])
293
+
294
+ # Actualizar el estado manualmente (inyectar información)
295
+ agent.update_state(config, {"messages": [HumanMessage(content="Nota: El sistema está en mantenimiento")]})
296
+ ```
297
+
298
+ ### Human-in-the-Loop (Interrupción y Aprobación)
299
+ Puedes interrumpir el grafo antes de ejecutar herramientas críticas para validación humana.
300
+
301
+ ```python
302
+ agent = create_react_agent(
303
+ llm,
304
+ tools=[herramientas_sensibles],
305
+ checkpointer=memory,
306
+ interrupt_before=["tools"] # Se detiene justo antes de llamar a la herramienta
307
+ )
308
+ ```
309
+
310
+ ---
311
+
312
+ ## 7. Integración con Model Context Protocol (MCP)
313
+
314
+ PhiChat soporta herramientas remotas de servidores MCP de forma nativa.
315
+
316
+ ```python
317
+ from PhiChat import ChatPhi
318
+ from langchain_mcp_adapters.client import MultiServerMCPClient
319
+
320
+ async def run_mcp():
321
+ async with MultiServerMCPClient({"db": {"transport": "stdio", "command": "python", "args": ["server.py"]}}) as client:
322
+ tools = await client.get_tools()
323
+ llm = ChatPhi()
324
+ # El loop agéntico maneja la ejecución asíncrona de MCP automáticamente
325
+ res = await llm.arun_tool_loop(messages=[...], tools=tools)
326
+ ```
327
+
328
+ ---
329
+
330
+ ## Configuración del modelo
331
+
332
+ | Parámetro | Default | Descripción |
333
+ |-----------|---------|-------------|
334
+ | `model` | `phi4` | Modelo Ollama a usar |
335
+ | `temperature` | `0.7` | Creatividad (0.0 para herramientas) |
336
+ | `num_ctx` | `8192` | Ventana de contexto |
337
+ | `keep_alive` | `0` | Tiempo de retención en RAM |
338
+
339
+ ```python
340
+ llm = ChatPhi(model="phi4:latest", temperature=0, num_ctx=16384)
341
+ ```