From dd3bde2ec95a5675fd3024837af4fd0367c643b5 Mon Sep 17 00:00:00 2001 From: acano Date: Thu, 19 Mar 2026 16:24:34 +0100 Subject: [PATCH] Add BEIR analysis notebooks for different datasets and models - Created `n00 Beir Analysis.ipynb` for analyzing BEIR dataset with Ollama embeddings. - Added `n00 Beir Analysis_cosqa.ipynb` for evaluating the CosQA dataset using similar embedding techniques. - Introduced `n00 first Analysis.ipynb` for initial analysis with Ragas embeddings and semantic similarity evaluation. - Implemented data loading and processing for each notebook, including downloading datasets and saving results. - Included evaluation metrics such as NDCG, MAP, Recall, and Precision for model performance assessment. --- research/agents/langgraph_agent_simple.ipynb | 835 ++++++++++++++ research/agents/n00 Agent Samples.ipynb | 544 +++++++++ .../n00 LangGrah Tool Calling Agent.ipynb | 1021 +++++++++++++++++ research/agents/n00 LangGraph Agent v1.ipynb | 318 +++++ research/agents/n00 LangGraph Agent v2.ipynb | 415 +++++++ .../agents/n00 LangGraph agent simple.ipynb | 472 ++++++++ .../agents/n00 langgraph_agent_simple.ipynb | 401 +++++++ .../embeddings/Embedding model selection.pdf | Bin 0 -> 105074 bytes .../beir_CodeXGlue_results.json | 54 + .../beir_Scifact_results.json | 62 + .../beir_cosqa_results.json | 62 + .../n00 Beir Analysis CodeXGlue.ipynb | 333 ++++++ research/embeddings/n00 Beir Analysis.ipynb | 323 ++++++ .../embeddings/n00 Beir Analysis_cosqa.ipynb | 335 ++++++ research/embeddings/n00 first Analysis.ipynb | 289 +++++ 15 files changed, 5464 insertions(+) create mode 100644 research/agents/langgraph_agent_simple.ipynb create mode 100644 research/agents/n00 Agent Samples.ipynb create mode 100644 research/agents/n00 LangGrah Tool Calling Agent.ipynb create mode 100644 research/agents/n00 LangGraph Agent v1.ipynb create mode 100644 research/agents/n00 LangGraph Agent v2.ipynb create mode 100644 research/agents/n00 LangGraph agent simple.ipynb create mode 100644 research/agents/n00 langgraph_agent_simple.ipynb create mode 100644 research/embeddings/Embedding model selection.pdf create mode 100644 research/embeddings/embedding_eval_results/beir_CodeXGlue_results.json create mode 100644 research/embeddings/embedding_eval_results/beir_Scifact_results.json create mode 100644 research/embeddings/embedding_eval_results/beir_cosqa_results.json create mode 100644 research/embeddings/n00 Beir Analysis CodeXGlue.ipynb create mode 100644 research/embeddings/n00 Beir Analysis.ipynb create mode 100644 research/embeddings/n00 Beir Analysis_cosqa.ipynb create mode 100644 research/embeddings/n00 first Analysis.ipynb diff --git a/research/agents/langgraph_agent_simple.ipynb b/research/agents/langgraph_agent_simple.ipynb new file mode 100644 index 0000000..8f98140 --- /dev/null +++ b/research/agents/langgraph_agent_simple.ipynb @@ -0,0 +1,835 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9f97dd1e", + "metadata": {}, + "source": [ + "# Libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "9e974df6", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "from typing import TypedDict, List, Optional, Annotated, Literal\n", + "from IPython.display import Image, display\n", + "\n", + "from langchain_core.documents import Document\n", + "from langchain_core.messages import BaseMessage, SystemMessage, AIMessage, ToolMessage\n", + "from langchain_core.tools import tool\n", + "from langgraph.checkpoint.memory import InMemorySaver\n", + "from langgraph.graph.message import add_messages\n", + "from langchain_elasticsearch import ElasticsearchStore\n", + "from langgraph.graph import StateGraph, END\n", + "from langgraph.prebuilt import ToolNode, tools_condition\n", + "from langfuse import Langfuse\n", + "from langfuse.decorators import observe, langfuse_context\n", + "\n", + "from src.utils.llm_factory import create_chat_model\n", + "from src.utils.emb_factory import create_embedding_model\n", + "from src.config import settings" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "30edcecc", + "metadata": {}, + "outputs": [], + "source": [ + "langfuse = Langfuse()\n", + "\n", + "llm = create_chat_model(\n", + " provider=\"ollama\",\n", + " model=settings.ollama_model_name,\n", + " temperature=0.5,\n", + " validate_model_on_init=True,\n", + ")\n", + "embeddings = create_embedding_model(\n", + " provider=\"ollama\",\n", + " model=settings.ollama_emb_model_name,\n", + ")\n", + "vector_store = ElasticsearchStore(\n", + " es_url=settings.elasticsearch_local_url,\n", + " index_name=\"avap-docs-test-v3\",\n", + " embedding=embeddings,\n", + " query_field=\"text\",\n", + " vector_query_field=\"embedding\",\n", + " # strategy=ElasticsearchStore.ApproxRetrievalStrategy(\n", + " # hybrid=True,\n", + " # rrf={\"rank_constant\": 60, \"window_size\": 100}\n", + " # )\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "ad98841b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Langfuse client is authenticated and ready!\n" + ] + } + ], + "source": [ + "if langfuse.auth_check():\n", + " print(\"Langfuse client is authenticated and ready!\")\n", + "else:\n", + " print(\"Authentication failed. Please check your credentials and host.\")" + ] + }, + { + "cell_type": "markdown", + "id": "873ea2f6", + "metadata": {}, + "source": [ + "### State" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5f8c88cf", + "metadata": {}, + "outputs": [], + "source": [ + "class AgentState(TypedDict):\n", + " messages: Annotated[list, add_messages]\n", + " reformulated_query: str\n", + " context: str" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "fd8ed542", + "metadata": {}, + "outputs": [], + "source": [ + "class AgenticAgentState(TypedDict):\n", + " messages: Annotated[list, add_messages]" + ] + }, + { + "cell_type": "markdown", + "id": "1d60c120", + "metadata": {}, + "source": [ + "### Tools" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "f0a21230", + "metadata": {}, + "outputs": [], + "source": [ + "retrieve_kwargs = {\"k\": 3}" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "f9359747", + "metadata": {}, + "outputs": [], + "source": [ + "def format_context(docs: List[Document]) -> str:\n", + " chunks: List[str] = []\n", + " for i, doc in enumerate(docs, 1):\n", + " source = (doc.metadata or {}).get(\"source\", \"Untitled\")\n", + " source_id = (doc.metadata or {}).get(\"id\", f\"chunk-{i}\")\n", + " text = doc.page_content or \"\"\n", + " chunks.append(f\"[{i}] id={source_id} source={source}\\n{text}\")\n", + " return \"\\n\\n\".join(chunks)\n", + "\n", + "\n", + "@tool\n", + "@observe(name=\"context_retrieve\")\n", + "def context_retrieve(query: str) -> str:\n", + " \"\"\"Consults vector store to respond AVAP related questions\n", + " Args:\n", + " query (str): The input query for which to retrieve relevant documents.\n", + " \"\"\"\n", + " retriever = vector_store.as_retriever(\n", + " search_type=\"similarity\",\n", + " search_kwargs=retrieve_kwargs,\n", + " )\n", + " docs = retriever.invoke(query)\n", + " context = format_context(docs)\n", + "\n", + " langfuse_context.update_current_observation(\n", + " input={\"query\": query, \"k\": retrieve_kwargs[\"k\"]},\n", + " output={\n", + " \"documents_count\": len(docs),\n", + " \"sources\": [(doc.metadata or {}).get(\"source\", \"Untitled\") for doc in docs],\n", + " \"document_ids\": [(doc.metadata or {}).get(\"id\", f\"chunk-{i+1}\") for i, doc in enumerate(docs)],\n", + " \"context_preview\": context[:1000],\n", + " },\n", + " )\n", + " return context" + ] + }, + { + "cell_type": "markdown", + "id": "395966e2", + "metadata": {}, + "source": [ + "### Agent" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "66ae23f0", + "metadata": {}, + "outputs": [], + "source": [ + "REFORMULATE_PROMPT = SystemMessage(\n", + " content=(\n", + " \"You are a deterministic query rewriting function.\\n\"\n", + " \"You convert natural language questions into keyword search queries.\\n\\n\"\n", + " \"Strict constraints:\\n\"\n", + " \"1. Keep function names and technical tokens unchanged.\\n\"\n", + " \"2. Remove filler phrases.\\n\"\n", + " \"3. Do not answer.\\n\"\n", + " \"4. Do not explain.\\n\"\n", + " \"5. Do not generate code.\\n\"\n", + " \"6. Return a single-line query only.\\n\"\n", + " \"7. If already optimal, return unchanged.\\n\"\n", + " )\n", + ")\n", + "\n", + "GENERATE_PROMPT = SystemMessage(\n", + " content=\"\"\"You are an agent designed to assist users with AVAP (Advanced Virtual API Programming) language.\n", + " It's a new language, so you should know nothing about it.\n", + " Use ONLY the provided context to answer AVAP-related questions.\n", + " If the context does not contain enough information, say so honestly.\n", + " If the question is not related to AVAP, answer based on your general knowledge.\n", + "\n", + " Context:\n", + " {context}\"\"\"\n", + ")\n", + "\n", + "AGENTIC_PROMPT = SystemMessage(\n", + " content=\"\"\"\n", + " You are an assistant that helps users with AVAP (Advanced Virtual API Programming) language questions.\n", + "\n", + " AVAP is a completely new programming language and you have NO built-in knowledge about it.\n", + "\n", + " Rules:\n", + "\n", + " 1. If the user question is related to AVAP:\n", + " - You must use the `context_retrieve` tool before answering.\n", + " - The tool output is INTERNAL CONTEXT, not a user message.\n", + " - Never thank the user for the retrieved context.\n", + " - You must synthesize an answer to respond the user query based SOLELY on the retrieved context.\n", + " \n", + " 2. If the retrieved context is insufficient:\n", + " - Call `context_retrieve` again with a better reformulated query.\n", + "\n", + " 3. If the question is not related to AVAP:\n", + " - Answer normally using general knowledge.\n", + "\n", + " 4. If the user asks for code only:\n", + " - Return only the code snippet.\n", + " - Do not add explanation, headings, or markdown fences unless requested.\n", + " \"\"\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "36d0f54e", + "metadata": {}, + "outputs": [], + "source": [ + "def reformulate(state: AgentState) -> AgentState:\n", + " \"\"\"Use the LLM to rewrite the user query for better retrieval.\"\"\"\n", + " user_msg = state[\"messages\"][-1]\n", + " resp = llm.invoke([REFORMULATE_PROMPT, user_msg])\n", + " reformulated = resp.content.strip()\n", + " print(f\"[reformulate] '{user_msg.content}' → '{reformulated}'\")\n", + " return {\"reformulated_query\": reformulated}\n", + "\n", + "\n", + "def retrieve(state: AgentState) -> AgentState:\n", + " \"\"\"Retrieve context using the reformulated query.\"\"\"\n", + " query = state[\"reformulated_query\"]\n", + " docs = vector_store.as_retriever(\n", + " search_type=\"similarity\",\n", + " search_kwargs=retrieve_kwargs,\n", + " ).invoke(query)\n", + " context = format_context(docs)\n", + " print(f\"[retrieve] {len(docs)} docs fetched\")\n", + " print(context)\n", + " return {\"context\": context}\n", + "\n", + "\n", + "def generate(state: AgentState) -> AgentState:\n", + " \"\"\"Generate the final answer using retrieved context.\"\"\"\n", + " prompt = SystemMessage(\n", + " content=GENERATE_PROMPT.content.format(context=state[\"context\"])\n", + " )\n", + " resp = llm.invoke([prompt] + state[\"messages\"])\n", + " return {\"messages\": [resp]}" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "f073edc9", + "metadata": {}, + "outputs": [], + "source": [ + "tools = [context_retrieve]\n", + "\n", + "def agent(state: AgenticAgentState) -> AgenticAgentState:\n", + " llm_with_tools = llm.bind_tools(tools)\n", + " return {\"messages\": [llm_with_tools.invoke([SystemMessage(content=AGENTIC_PROMPT.content)] + state[\"messages\"])]}" + ] + }, + { + "cell_type": "markdown", + "id": "ef55bca3", + "metadata": {}, + "source": [ + "### Graph" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "fae46a58", + "metadata": {}, + "outputs": [], + "source": [ + "memory = InMemorySaver()\n", + "\n", + "graph_builder = StateGraph(AgentState)\n", + "\n", + "graph_builder.add_node(\"reformulate\", reformulate)\n", + "graph_builder.add_node(\"retrieve\", retrieve)\n", + "graph_builder.add_node(\"generate\", generate)\n", + "\n", + "graph_builder.set_entry_point(\"reformulate\")\n", + "graph_builder.add_edge(\"reformulate\", \"retrieve\")\n", + "graph_builder.add_edge(\"retrieve\", \"generate\")\n", + "graph_builder.add_edge(\"generate\", END)\n", + "\n", + "guided_graph = graph_builder.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "7f57b543", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "try:\n", + " display(Image(guided_graph.get_graph().draw_mermaid_png()))\n", + "except Exception:\n", + " pass" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "f7a0993f", + "metadata": {}, + "outputs": [], + "source": [ + "tool_node = ToolNode(tools=tools)\n", + "memory = InMemorySaver()\n", + "\n", + "graph_builder = StateGraph(AgenticAgentState)\n", + "\n", + "graph_builder.add_node(\"agent\", agent)\n", + "graph_builder.add_node(\"tools\", tool_node)\n", + "\n", + "graph_builder.set_entry_point(\"agent\")\n", + "graph_builder.add_conditional_edges(\n", + " \"agent\",\n", + " tools_condition,\n", + ")\n", + "graph_builder.add_edge(\"tools\", \"agent\")\n", + "\n", + "agentic_graph = graph_builder.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "2fec3fdb", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "try:\n", + " display(Image(agentic_graph.get_graph().draw_mermaid_png()))\n", + "except Exception:\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "1e9aff05", + "metadata": {}, + "source": [ + "### Test" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "8569cf39", + "metadata": {}, + "outputs": [], + "source": [ + "@observe(name=\"graph_run\")\n", + "def stream_graph_updates(user_input: str, graph: StateGraph):\n", + " langfuse_context.update_current_trace(\n", + " user_id=\"alberto\",\n", + " tags=[\"avap\", \"rag\", \"langgraph\"],\n", + " metadata={\"feature\": \"agentic-rag\"},\n", + " )\n", + "\n", + " for event in graph.stream(\n", + " {\"messages\": [{\"role\": \"user\", \"content\": user_input}]},\n", + " stream_mode=\"values\",\n", + " ):\n", + " event[\"messages\"][-1].pretty_print()\n", + "\n", + " return event[\"messages\"][-1]" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "a1a1f3cf", + "metadata": {}, + "outputs": [], + "source": [ + "user_input = \"\"\"What types of includes does AVAP have?\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "53b89690", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "What types of includes does AVAP have?\n", + "[reformulate] 'What types of includes does AVAP have?' → '\"avap includes type\"'\n", + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "What types of includes does AVAP have?\n", + "[retrieve] 3 docs fetched\n", + "[1] id=chunk-1 source=Untitled\n", + "\n", + "\n", + "Token:\n", + "\n", + "\n", + "\n", + "ASSIGN\n", + "\n", + "[2] id=chunk-2 source=Untitled\n", + "\n", + "\n", + "> **Nota de implementación:** `` se distingue de `` (ORM) únicamente por contexto semántico: el UUID pasado como argumento determina si el adaptador resuelto es un ORM de base de datos o un proxy de terceros. La gramática los trata de forma idéntica; el motor de ejecución selecciona el adaptador apropiado en runtime.\n", + "\n", + "---\n", + "\n", + "## SECCIÓN VI: Utilidades, Criptografía y Manipulación de Datos\n", + "\n", + "AVAP incluye un set de comandos integrados de alto nivel para manipular tipos complejos (JSON y Listas), tiempos, textos y generar hashes.\n", + "\n", + "### 6.1 Manipulación Nativa de Listas y Objetos JSON\n", + "Para extraer y mutar estructuras complejas, AVAP provee comandos nativos específicos:\n", + "* **`variableToList(elemento, destino)`**: Fuerza a que una variable escalar se convierta en una estructura iterable de lista.\n", + "* **`itemFromList(lista_origen, indice, destino)`**: Extrae de forma segura el elemento contenido en la posición `indice` de una lista.\n", + "* **`variableFromJSON(json_origen, clave, destino)`**: Parsea un objeto JSON en memoria y extrae el valor correspondiente a la `clave`.\n", + "* **`AddVariableToJSON(clave, valor, json_destino)`**: Inyecta dinámicamente una nueva propiedad dentro de un objeto JSON existente.\n", + "\n", + "### 6.2 Criptografía y Expresiones Regulares\n", + "* **`encodeSHA256` y `encodeMD5(origen, destino)`**: Funciones criptográficas que encriptan de forma irreversible un texto. Vitales para el almacenamiento seguro de contraseñas.\n", + "* **`getRegex(origen, patron, destino)`**: Aplica una Expresión Regular (`patron`) sobre la variable de origen, extrayendo las coincidencias exactas.\n", + "\n", + "### 6.3 Transformación de Tiempo y Cadenas\n", + "* **Fechas:** `getTimeStamp` (convierte un string a Epoch), `getDateTime` (Epoch a string legible), y `stampToDatetime` (Epoch a objeto datetime estructurado). Soportan formatos de calendario y cálculos con TimeDeltas.\n", + "* **Cadenas:** `replace` (saneamiento y sustitución de texto) y `randomString` (generación determinista de claves/tokens aleatorios).\n", + "\n", + "### Especificación BNF (Sección VI)\n", + "\n", + "\n", + "\n", + "/* [CORRECCIÓN] Todas las subreglas de están ahora completamente expandidas. */\n", + " ::= | | | | | | \n", + "\n", + "/* Manipulación de listas y JSON */\n", + " ::= \"variableToList(\" \",\" \")\"\n", + " | \"itemFromList(\" \",\" \",\" \")\"\n", + " | \"variableFromJSON(\" \",\" \",\" \")\"\n", + " | \"AddVariableToJSON(\" \",\" \",\" \")\"\n", + "\n", + "/* Criptografía */\n", + " ::= \"encodeSHA256(\" \",\" \")\"\n", + " | \"encodeMD5(\" \",\" \")\"\n", + "\n", + "/* Expresiones regulares */\n", + " ::= \"getRegex(\" \",\" \",\" \")\"\n", + "\n", + " ::= \"getDateTime(\" \",\" \",\" \",\" \")\"\n", + "/* Argumentos: formato_salida, epoch_origen, zona_horaria, destino */\n", + "\n", + " ::= \"stampToDatetime(\" \",\" \",\" \",\" \")\"\n", + "/* Argumentos: epoch_origen, formato, timedelta, destino */\n", + " | \"getTimeStamp(\" \",\" \",\" \",\" \")\"\n", + "/* Argumentos: fecha_string, formato_entrada, timedelta, destino */\n", + "\n", + " ::= \"randomString(\" \",\" \")\"\n", + "/* Argumentos: longitud, destino */\n", + "\n", + " ::= \"replace(\" \",\" \",\" \",\" \")\"\n", + "/* Argumentos: origen, patron_busqueda, reemplazo, destino */\n", + "\n", + "[3] id=chunk-3 source=Untitled\n", + "\n", + "\n", + "---\n", + "\n", + "## SECCIÓN IX: Expresiones y Gramática Léxica Estricta\n", + "\n", + "Esta sección es el corazón matemático evaluador de AVAP. Define la jerarquía exacta (Precedencia) y provee soporte nativo para características avanzadas similares a Python.\n", + "\n", + "### 9.1 Cast de Tipos Explícito\n", + "AVAP permite conversiones de tipos (Type Casting) en cualquier evaluación utilizando funciones constructoras estándar. Puedes transformar variables dinámicamente usando `int(var)`, `float(var)` o `str(var)`.\n", + "\n", + "### 9.2 Slicing y Comprensiones (Comprehensions)\n", + "* **Slicing (Cortes):** Puedes extraer fragmentos de listas o strings utilizando la notación de dos puntos. Ejemplo: `mi_lista[1:4]` (extrae desde el índice 1 hasta el 3).\n", + "* **Comprehensions:** AVAP soporta la construcción rápida de listas mediante iteradores en una sola línea, permitiendo filtrar y mapear colecciones enteras (ej. `[x * 2 for x in valores if x > 0]`).\n", + "\n", + "### 9.3 Análisis Léxico (Lexer) y Documentación\n", + "AVAP cuenta con tres niveles de descarte de texto para anotaciones humanas:\n", + "1. **Comentarios de Línea (`//`):** Ignora el texto hasta el salto de línea.\n", + "2. **Comentarios de Bloque (`/* ... */`):** Para aislar bloques enteros multilínea.\n", + "3. **Comentarios de Documentación (`///`):** Utilizados por analizadores de código o IDEs para generar documentación técnica automática (Docstrings) a partir del código fuente.\n", + "\n", + "### Especificación BNF (Sección IX)\n", + "\n", + "\n", + "\n", + "/* Jerarquía de Expresiones (Precedencia de menor a mayor) */\n", + " ::= \n", + " ::= ( \"or\" )*\n", + " ::= ( \"and\" )*\n", + " ::= \"not\" | \n", + "\n", + " ::= ( )*\n", + " ::= \"==\" | \"!=\" | \"<\" | \">\" | \"<=\" | \">=\" | \"in\" | \"is\"\n", + "\n", + " ::= ( ( \"+\" | \"-\" ) )*\n", + " ::= ( ( \"*\" | \"/\" | \"%\" ) )*\n", + " ::= ( \"+\" | \"-\" ) | \n", + " ::= [ \"**\" ]\n", + "\n", + "/* Primarios y Átomos (Accesos, Castings, Slicing, Métodos y Funciones)\n", + " La regla cubre también el acceso a métodos de objetos conector\n", + " (conector.metodo(...)) y el acceso por clave a sus resultados (resultado[\"key\"]) */\n", + " ::= \n", + " | \".\" \n", + " | \"[\" \"]\"\n", + " | \"[\" [] \":\" [] [\":\" []] \"]\"\n", + " | \"(\" [] \")\"\n", + "\n", + " ::= \n", + " | \"$\" \n", + " | \n", + " | \"(\" \")\"\n", + " | \n", + " | \n", + "\n", + "/* Estructuras de Datos, Comprensiones y Argumentos */\n", + " ::= \"[\" [] \"]\"\n", + " | \"[\" \"for\" \"in\" [] \"]\"\n", + " ::= \"if\" \n", + " ::= \"{\" [] \"}\"\n", + " ::= ( \",\" )*\n", + " ::= \":\" \n", + " ::= ( \",\" )*\n", + "\n", + "/* Tipo numérico unificado */\n", + " ::= | \n", + "\n", + "/* Literales (Tipos de Datos Primitivos Soportados) */\n", + " ::= | | | \"None\"\n", + " ::= \"True\" | \"False\"\n", + " ::= [0-9]+\n", + " ::= [0-9]+ \".\" [0-9]* | \".\" [0-9]+\n", + "\n", + "/* Cadenas de Texto con soporte de secuencias de escape */\n", + " ::= \"\\\"\" \"\\\"\" | \"'\" \"'\"\n", + " ::= \"\\\\\" ( \"\\\"\" | \"'\" | \"\\\\\" | \"n\" | \"t\" | \"r\" | \"0\" )\n", + " ::= ( [^\"\\\\] | )*\n", + " ::= ( [^'\\\\] | )*\n", + " ::= | \n", + "\n", + "/* Reglas de Comentarios para el Lexer\n", + " El lexer aplica longest-match: /// debe evaluarse ANTES que // */\n", + " ::= \"///\" \n", + " ::= \"//\" \n", + " ::= \"/*\" \"*/\"\n", + " ::= [^\\r\\n]*\n", + " ::= /* Cualquier secuencia de caracteres que no contenga la subcadena \"*/\" */\n", + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "What types of includes does AVAP have?\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "AVAP has two main types of include:\n", + "\n", + "1. **:** This is used to instantiate a connector, which could be for a database connection or a third-party API.\n", + "2. **:** This term seems to be related to initializing an Object-Relational Mapping (ORM) connector, indicating that the context suggests it's part of a specific ORM setup.\n", + "\n", + "Both types are treated similarly in terms of grammar but differ semantically by their purpose - one is for database connections or third-party APIs, while the other is specifically for connecting to ORMs. The engine selects the appropriate adapter based on runtime context.\n" + ] + } + ], + "source": [ + "a = stream_graph_updates(user_input, guided_graph)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d6b4da6a", + "metadata": {}, + "outputs": [], + "source": [ + "result = agentic_graph.invoke({\"messages\": [{\"role\": \"user\", \"content\": user_input}]})\n", + "print(\"Final result:\")\n", + "result[\"messages\"][-1].pretty_print()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2342b1f1", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "3707574b", + "metadata": {}, + "source": [ + "### MTEB" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "d9657ec4", + "metadata": {}, + "outputs": [ + { + "ename": "SyntaxError", + "evalue": "invalid syntax (2793878467.py, line 12)", + "output_type": "error", + "traceback": [ + " \u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[18]\u001b[39m\u001b[32m, line 12\u001b[39m\n\u001b[31m \u001b[39m\u001b[31mnorms = np.linalg.norm(x, axis=1, keepdims=True)agent_graph\u001b[39m\n ^\n\u001b[31mSyntaxError\u001b[39m\u001b[31m:\u001b[39m invalid syntax\n" + ] + } + ], + "source": [ + "from dataclasses import dataclass\n", + "from typing import Any, Iterable\n", + " \n", + "import numpy as np\n", + " \n", + "import mteb\n", + "from mteb.types import Array\n", + "from mteb.models import SearchEncoderWrapper\n", + " \n", + " \n", + "def _l2_normalize(x: np.ndarray, eps: float = 1e-12) -> np.ndarray:\n", + " norms = np.linalg.norm(x, axis=1, keepdims=True)agent_graph\n", + " return x / np.clip(norms, eps, None)\n", + " \n", + " \n", + "def _to_text_list(batch: dict[str, Any]) -> list[str]:\n", + " \"\"\"\n", + " MTEB batched inputs can be:\n", + " - TextInput: {\"text\": [..]}\n", + " - CorpusInput: {\"title\": [..], \"body\": [..], \"text\": [..]}\n", + " - QueryInput: {\"query\": [..], \"instruction\": [..], \"text\": [..]}\n", + " We prefer \"text\" if present; otherwise compose from title/body or query/instruction.\n", + " \"\"\"\n", + " if \"text\" in batch and batch[\"text\"] is not None:\n", + " return list(batch[\"text\"])\n", + " \n", + " if \"title\" in batch and \"body\" in batch:\n", + " titles = batch[\"title\"] or [\"\"] * len(batch[\"body\"])\n", + " bodies = batch[\"body\"] or [\"\"] * len(batch[\"title\"])\n", + " return [f\"{t} {b}\".strip() for t, b in zip(titles, bodies)]\n", + " \n", + " if \"query\" in batch:\n", + " queries = list(batch[\"query\"])\n", + " instructions = batch.get(\"instruction\")\n", + " if instructions:\n", + " return [f\"{i} {q}\".strip() for q, i in zip(queries, instructions)]\n", + " return queries\n", + " \n", + " raise ValueError(f\"Unsupported batch keys: {sorted(batch.keys())}\")\n", + " \n", + " \n", + "@dataclass\n", + "class OllamaLangChainEncoder:\n", + " lc_embeddings: Any # OllamaEmbeddings implements embed_documents()\n", + " normalize: bool = True\n", + " \n", + " # Optional metadata hook used by some wrappers; safe to keep as None for local runs\n", + " mteb_model_meta: Any = None\n", + " \n", + " def encode(\n", + " self,\n", + " inputs: Iterable[dict[str, Any]],\n", + " *,\n", + " task_metadata: Any,\n", + " hf_split: str,\n", + " hf_subset: str,\n", + " prompt_type: Any = None,\n", + " **kwargs: Any,\n", + " ) -> Array:\n", + " all_vecs: list[np.ndarray] = []\n", + " \n", + " for batch in inputs:\n", + " texts = _to_text_list(batch)\n", + " vecs = self.lc_embeddings.embed_documents(texts)\n", + " arr = np.asarray(vecs, dtype=np.float32)\n", + " if self.normalize:\n", + " arr = _l2_normalize(arr)\n", + " all_vecs.append(arr)\n", + " \n", + " if not all_vecs:\n", + " return np.zeros((0, 0), dtype=np.float32)\n", + " \n", + " return np.vstack(all_vecs)\n", + " \n", + " def similarity(self, embeddings1: Array, embeddings2: Array) -> Array:\n", + " a = np.asarray(embeddings1, dtype=np.float32)\n", + " b = np.asarray(embeddings2, dtype=np.float32)\n", + " if self.normalize:\n", + " # dot == cosine if already normalized\n", + " return a @ b.T\n", + " a = _l2_normalize(a)\n", + " b = _l2_normalize(b)\n", + " return a @ b.T\n", + " \n", + " def similarity_pairwise(self, embeddings1: Array, embeddings2: Array) -> Array:\n", + " a = np.asarray(embeddings1, dtype=np.float32)\n", + " b = np.asarray(embeddings2, dtype=np.float32)\n", + " if not self.normalize:\n", + " a = _l2_normalize(a)\n", + " b = _l2_normalize(b)\n", + " return np.sum(a * b, axis=1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "85727a68", + "metadata": {}, + "outputs": [], + "source": [ + "encoder = OllamaLangChainEncoder(lc_embeddings=embeddings, normalize=True)\n", + "search_model = SearchEncoderWrapper(encoder)\n", + " \n", + "tasks = mteb.get_tasks([\n", + " \"CodeSearchNetRetrieval\",\n", + " \"CodeSearchNetCCRetrieval\",\n", + " \"AppsRetrieval\",\n", + " \"StackOverflowDupQuestions\",\n", + "])\n", + "results = mteb.evaluate(\n", + " model=search_model,\n", + " tasks=tasks,\n", + " encode_kwargs={\"batch_size\": 32, \"show_progress_bar\": True}\n", + ")\n", + " \n", + "print(results)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4052f229", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "assistance-engine", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/research/agents/n00 Agent Samples.ipynb b/research/agents/n00 Agent Samples.ipynb new file mode 100644 index 0000000..7644dd4 --- /dev/null +++ b/research/agents/n00 Agent Samples.ipynb @@ -0,0 +1,544 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7cf31ec4", + "metadata": {}, + "source": [ + "# Libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "id": "5fe736bf", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import sys\n", + "from pathlib import Path\n", + "from typing import TypedDict, List, Optional, Annotated, Literal\n", + "from IPython.display import Image, display\n", + "from pydantic import BaseModel, Field\n", + "\n", + "# Ensure the project root is on the path so `src` is importable\n", + "_project_root = str(Path(__file__).resolve().parents[2]) if \"__file__\" in dir() else str(Path.cwd().parents[1])\n", + "if _project_root not in sys.path:\n", + " sys.path.insert(0, _project_root)\n", + "\n", + "from langchain_core.documents import Document\n", + "from langchain_core.messages import BaseMessage, SystemMessage, AIMessage\n", + "from langchain_core.tools import tool\n", + "from langgraph.checkpoint.memory import InMemorySaver\n", + "from langgraph.graph.message import add_messages\n", + "from langchain_ollama import ChatOllama, OllamaEmbeddings\n", + "from langchain_elasticsearch import ElasticsearchStore\n", + "from langgraph.graph import StateGraph, END\n", + "from langgraph.prebuilt import ToolNode, tools_condition\n", + "from langfuse import Langfuse\n", + "\n", + "from src.utils.llm_factory import create_chat_model\n", + "from src.utils.emb_factory import create_embedding_model\n", + "from src.config import (\n", + " ELASTICSEARCH_LOCAL_URL,\n", + " ELASTICSEARCH_INDEX,\n", + " OLLAMA_MODEL_NAME,\n", + " OLLAMA_EMB_MODEL_NAME\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "a73629e1", + "metadata": {}, + "source": [ + "# State" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "id": "5097b8a5", + "metadata": {}, + "outputs": [], + "source": [ + "class AgentState(TypedDict):\n", + " messages: Annotated[list, add_messages]\n", + " reformulated_query: str\n", + " context: str" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "id": "c5328a0d", + "metadata": {}, + "outputs": [], + "source": [ + "class AgenticAgentState(TypedDict):\n", + " messages: Annotated[list, add_messages]" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "id": "e8f0a0bc", + "metadata": {}, + "outputs": [], + "source": [ + "REFORMULATE_PROMPT = SystemMessage(\n", + " content=(\n", + " \"You are a deterministic query rewriting function.\\n\"\n", + " \"You convert natural language questions into keyword search queries.\\n\\n\"\n", + " \"Strict constraints:\\n\"\n", + " \"1. Keep function names and technical tokens unchanged.\\n\"\n", + " \"2. Remove filler phrases.\\n\"\n", + " \"3. Do not answer.\\n\"\n", + " \"4. Do not explain.\\n\"\n", + " \"5. Do not generate code.\\n\"\n", + " \"6. Return a single-line query only.\\n\"\n", + " \"7. If already optimal, return unchanged.\\n\"\n", + " )\n", + ")\n", + "\n", + "GENERATE_PROMPT = SystemMessage(\n", + " content=\"\"\"You are an agent designed to assist users with AVAP (Advanced Virtual API Programming) language.\n", + " It's a new language, so you should know nothing about it.\n", + " Use ONLY the provided context to answer AVAP-related questions.\n", + " If the context does not contain enough information, say so honestly.\n", + " If the question is not related to AVAP, answer based on your general knowledge.\n", + "\n", + " Context:\n", + " {context}\"\"\"\n", + ")\n", + "\n", + "AGENTIC_PROMPT = SystemMessage(\n", + " content=\"\"\"You are an agent designed to assist users with AVAP (Advanced Virtual API Programming) language.\n", + " It's a new language, so you should know nothing about it.\n", + " Use ONLY the provided 'context_retrieve' tool to answer AVAP-related questions.\n", + " The 'context_retrieve' tool receives a user query (as a string) and returns relevant context from a vector store.\n", + " If the context does not contain enough information, say so honestly.\n", + " If the question is not related to AVAP, answer based on your general knowledge.\n", + " \"\"\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "ebd003d9", + "metadata": {}, + "source": [ + "# Function" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "id": "b4e5d981", + "metadata": {}, + "outputs": [], + "source": [ + "def format_context(docs: List[Document]) -> str:\n", + " chunks: List[str] = []\n", + " for i, doc in enumerate(docs, 1):\n", + " source = (doc.metadata or {}).get(\"source\", \"Untitled\")\n", + " source_id = (doc.metadata or {}).get(\"id\", f\"chunk-{i}\")\n", + " text = doc.page_content or \"\"\n", + " chunks.append(f\"[{i}] id={source_id} source={source}\\n{text}\")\n", + " return \"\\n\\n\".join(chunks)" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "id": "8a38359a", + "metadata": {}, + "outputs": [], + "source": [ + "def reformulate(state: AgentState, llm=None) -> AgentState:\n", + " \"\"\"Use the LLM to rewrite the user query for better retrieval.\"\"\"\n", + " # The graph runner passes the state only. Accept an optional `llm` so\n", + " # this function can also be called directly with an explicit model.\n", + " user_msg = state[\"messages\"][-1]\n", + " if llm is None:\n", + " llm = globals().get('llm')\n", + " if llm is None:\n", + " raise RuntimeError('No LLM available for reformulate')\n", + " resp = llm.invoke([REFORMULATE_PROMPT, user_msg])\n", + " reformulated = resp.content.strip()\n", + " print(f\"[reformulate] '{user_msg.content}' → '{reformulated}'\")\n", + " return {\"reformulated_query\": reformulated}\n", + "\n", + "\n", + "def retrieve(state: AgentState, vector_store=None, retrieve_kwargs=None) -> AgentState:\n", + " \"\"\"Retrieve context using the reformulated query.\"\"\"\n", + " # Graph runner passes state first. Accept optional `vector_store` and\n", + " # `retrieve_kwargs` for direct calls; otherwise fall back to globals.\n", + " if vector_store is None:\n", + " vector_store = globals().get('vector_store')\n", + " if vector_store is None:\n", + " raise RuntimeError('No vector_store available for retrieve')\n", + " if retrieve_kwargs is None:\n", + " retrieve_kwargs = {}\n", + " query = state[\"reformulated_query\"]\n", + " docs = vector_store.as_retriever(\n", + " search_type=\"similarity\",\n", + " search_kwargs=retrieve_kwargs,\n", + " ).invoke(query)\n", + " context = format_context(docs)\n", + " print(f\"[retrieve] {len(docs)} docs fetched\")\n", + " print(context)\n", + " return {\"context\": context}\n", + "\n", + "\n", + "def generate(llm, state: AgentState) -> AgentState:\n", + " \"\"\"Generate the final answer using retrieved context.\"\"\"\n", + " prompt = SystemMessage(\n", + " content=GENERATE_PROMPT.content.format(context=state[\"context\"])\n", + " )\n", + " resp = llm.invoke([prompt] + state[\"messages\"])\n", + " return {\"messages\": [resp]}" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "d5001041", + "metadata": {}, + "outputs": [], + "source": [ + "def agent(llm, tools, state: AgentState) -> AgentState:\n", + " llm_with_tools = llm.bind_tools(tools)\n", + " return {\"messages\": [llm_with_tools.invoke([SystemMessage(content=AGENTIC_PROMPT.content)] + state[\"messages\"])]}" + ] + }, + { + "cell_type": "markdown", + "id": "538f812d", + "metadata": {}, + "source": [ + "# Code" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "id": "89786481", + "metadata": {}, + "outputs": [], + "source": [ + "langfuse = Langfuse()\n", + "\n", + "llm = create_chat_model(\n", + " provider=\"ollama\",\n", + " model=OLLAMA_MODEL_NAME,\n", + " temperature=0,\n", + " validate_model_on_init=True,\n", + ")\n", + "embeddings = create_embedding_model(\n", + " provider=\"ollama\",\n", + " model=OLLAMA_EMB_MODEL_NAME,\n", + ")\n", + "vector_store = ElasticsearchStore(\n", + " es_url=ELASTICSEARCH_LOCAL_URL,\n", + " index_name=ELASTICSEARCH_INDEX,\n", + " embedding=embeddings,\n", + " query_field=\"text\",\n", + " vector_query_field=\"vector\",\n", + " # strategy=ElasticsearchStore.ApproxRetrievalStrategy(\n", + " # hybrid=True,\n", + " # rrf={\"rank_constant\": 60, \"window_size\": 100}\n", + " # )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "22115cdc", + "metadata": {}, + "source": [ + "## Tool" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "42df0acc", + "metadata": {}, + "outputs": [], + "source": [ + "retrieve_kwargs = {\"k\": 3}" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "id": "a132e0e7", + "metadata": {}, + "outputs": [], + "source": [ + "@tool\n", + "def context_retrieve(query: str) -> str:\n", + " \"\"\"Consults vector store to respond AVAP related questions\n", + " Args:\n", + " query (str): The input query for which to retrieve relevant documents.\n", + " \"\"\"\n", + " retriever = vector_store.as_retriever(\n", + " search_type=\"similarity\",\n", + " search_kwargs=retrieve_kwargs,\n", + " )\n", + " docs = retriever.invoke(query)\n", + " return format_context(docs)" + ] + }, + { + "cell_type": "markdown", + "id": "0165d8d7", + "metadata": {}, + "source": [ + "## Graph" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "id": "70fdd80f", + "metadata": {}, + "outputs": [], + "source": [ + "memory = InMemorySaver()\n", + "\n", + "graph_builder = StateGraph(AgentState)\n", + "\n", + "graph_builder.add_node(\"reformulate\", reformulate)\n", + "graph_builder.add_node(\"retrieve\", retrieve)\n", + "graph_builder.add_node(\"generate\", generate)\n", + "\n", + "graph_builder.set_entry_point(\"reformulate\")\n", + "graph_builder.add_edge(\"reformulate\", \"retrieve\")\n", + "graph_builder.add_edge(\"retrieve\", \"generate\")\n", + "graph_builder.add_edge(\"generate\", END)\n", + "\n", + "guided_graph = graph_builder.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "id": "be526413", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "try:\n", + " display(Image(guided_graph.get_graph().draw_mermaid_png()))\n", + "except Exception:\n", + " pass" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "id": "fe35e0c0", + "metadata": {}, + "outputs": [], + "source": [ + "tools = [context_retrieve]\n", + "tool_node = ToolNode(tools=tools)\n", + "memory = InMemorySaver()\n", + "\n", + "graph_builder = StateGraph(AgenticAgentState)\n", + "\n", + "graph_builder.add_node(\"agent\", agent)\n", + "graph_builder.add_node(\"tools\", tool_node)\n", + "\n", + "graph_builder.set_entry_point(\"agent\")\n", + "graph_builder.add_conditional_edges(\n", + " \"agent\",\n", + " tools_condition,\n", + ")\n", + "graph_builder.add_edge(\"tools\", \"agent\")\n", + "\n", + "agentic_graph = graph_builder.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "id": "f48a5599", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "try:\n", + " display(Image(agentic_graph.get_graph().draw_mermaid_png()))\n", + "except Exception:\n", + " pass" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "id": "4626c9cb", + "metadata": {}, + "outputs": [], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"5\"}, \n", + " #\"callbacks\": [langfuse_handler],\n", + " #\"run_name\": \"rag-local-test\"differences between getDatetime() and getTimeStamp() functions in AVAP\n", + " }\n", + "\n", + "def stream_graph_updates(user_input: str, graph: StateGraph):\n", + " for event in graph.stream(\n", + " {\"messages\": [{\"role\": \"user\", \"content\": user_input}]},\n", + " #config=config,\n", + " stream_mode=\"values\",\n", + " ):\n", + " event[\"messages\"][-1].pretty_print()\n", + " # last_msg = event[\"messages\"][-1]\n", + " # if isinstance(last_msg, AIMessage):\n", + " # last_msg.pretty_print()" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "id": "ec8fe643", + "metadata": {}, + "outputs": [], + "source": [ + "user_input = \"\"\"Suppose you want to create a table called users in a database called myDatabase, with two columns: username of type VARCHAR and age of type INTEGER. How would you do that in AVAP?\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ec565fa2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "Suppose you want to create a table called users in a database called myDatabase, with two columns: username of type VARCHAR and age of type INTEGER. How would you do that in AVAP?\n", + "[reformulate] 'Suppose you want to create a table called users in a database called myDatabase, with two columns: username of type VARCHAR and age of type INTEGER. How would you do that in AVAP?' → '```sql\n", + "CREATE TABLE IF NOT EXISTS `myDatabase`.`users` (\n", + " `username` VARCHAR(255) NULL,\n", + " `age` INT NULL,\n", + " PRIMARY KEY (`username`)\n", + ");\n", + "```'\n", + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "Suppose you want to create a table called users in a database called myDatabase, with two columns: username of type VARCHAR and age of type INTEGER. How would you do that in AVAP?\n", + "[retrieve] 4 docs fetched\n", + "[1] id=chunk-1 source=21_Persistance_connectors_orm.txt\n", + "SECTION V: Persistence, Connectors, and Native ORM AVAP is designed to be database-agnostic. It enables data manipulation through three layers: the universal connector, simplified ORM commands, and direct SQL execution. 5.1 The Universal Connector (avapConnector) The avapConnector command is the entry point for any external integration. It uses a Connection Token system (Base64) that encapsulates configuration details (host, port, credentials, driver) to keep code clean and secure. Interface connector_variable = avapConnector(\"BASE64_TOKEN\") Connector Object Capabilities Once instantiated, the variable behaves as an object with dynamic methods: Database Connectors: Expose the .query(sql_string) method, which returns objects or lists depending on the result set. API Connectors (Twilio, Slack, etc.): Expose native service methods (e.g., .send_sms()). Example: Dynamic Assignment with Connectors // Instantiate the connection db = avapConnector(\"REJfQ09OTkVDVE9SM...\") // Execute query and use Section I dynamic evaluation users = db.query(\"SELECT * FROM users\") first_admin = users[0].name if users[0].role == 'admin' else 'N/A' addResult(first_admin) 5.2 Native ORM Layer (ormCheckTable / ormDirect) For quick operations on the local or default database cluster, AVAP provides system-level commands that do not require prior instantiation. 5.2.1 ormCheckTable Verifies the existence of a database structure. It is critical for installation scripts or automated migrations. Interface: ormCheckTable(table_name, target_var) Response: target_var receives the string values \"True\" or \"False\". 5.2.2 ormDirect Executes SQL statements directly. Unlike .query(), it is optimized for statements that do not necessarily return rows (such as INSERT, UPDATE, or CREATE TABLE). Interface: ormDirect(statement, target_var) Interpolation Usage Example: ormDirect(\"UPDATE users SET login = '%s' WHERE id = %s\" % (now, id), result) 5.3 Data Access Abstraction (Implicit Commands) AVAP includes specialized commands for common CRUD operations, reducing the need to write manual SQL and mitigating injection risks. ormAccessSelect Performs filtered queries returning a list-of-objects structure. Syntax: ormAccessSelect(table, filters, target) ormAccessInsert / ormAccessUpdate Manages data persistence. If used on an object that already has an ID, Update synchronizes changes; otherwise, Insert creates the record. 5.4 Dynamic Query Formatting (Injection Prevention) As detailed in Section I, the AVAP engine processes SQL strings before sending them to the database engine. The official recommendation is to always use interpolation with the % operator to ensure proper handling of data types (Strings vs Integers) by the driver. Recommended Secure Pattern sql = \"SELECT * FROM %s WHERE status = '%s'\" % (table_name, recovered_status) res = db.query(sql) 5.5 Cryptographic Security Integration (encodeSHA256) Within the persistence flow, AVAP provides native tools to secure sensitive data before it is written to disk. Interface encodeSHA256(source_text, target_variable) Complete Registration Flow (Final Example) This example integrates Sections I, II, III, and V: // II: Input capture addParam(\"pass\", p) addParam(\"user\", u) // I & V: Processing and security encodeSHA256(p, secure_pass) // V: Insertion sql = \"INSERT INTO users (username, password) VALUES ('%s', '%s')\" % (u, secure_pass) ormDirect(sql, db_result) // III & II: Response if(db_result, \"Success\", \"=\") addVar(msg, \"User created\") addResult(msg) end() Examples 1. Connector Instantiation Code snippet my_db = avapConnector(\"VE9LRU5fREVCX0RFU0FSUk9MTE8=\") 2. Record Retrieval Code snippet rows = my_db.query(\"SELECT id, name FROM users\") addResult(rows) 3. Direct Command Execution Code snippet ormDirect(\"TRUNCATE TABLE temp_cache\", status) 4. Structure Verification Code snippet ormCheckTable(\"inventory\", exists) if(exists, \"False\", \"==\") ormDirect(\"CREATE TABLE inventory...\", r) end() 5. Secure Update (Interpolation) Code snippet sql = \"UPDATE users SET login_count = %s WHERE email = '%s'\" % (count, email) ormDirect(sql, res) 6. JSON/DB Object Navigation Code snippet found_id = query_result[0].id addResult(found_id) 7. ORM Select with Filter Code snippet ormAccessSelect(\"orders\", {\"status\": \"pending\"}, list_result) addResult(list_result) 8. Processing Database Results Code snippet records = db.query(\"SELECT...\") startLoop(i, 0, len(records)) name = records[i].name endLoop() 9. Cryptographic Persistence Code snippet encodeSHA256(password_raw, hashed) ormDirect(\"INSERT INTO logins (hash) VALUES ('%s')\" % hashed, r) 10. Third-Party Connector (e.g., Slack) Code snippet slack_api = avapConnector(\"U0xBQ0tfQVBJX1RPS0VO\")\n", + "\n", + "[2] id=chunk-2 source=22_System_utilities_transformation.txt\n", + "SECTION VI: System Utilities and Transformation This section documents the native commands for advanced string manipulation, precise time handling, and dynamic data generation. 6.1 Time and Date Management (getDateTime / stampToDatetime) AVAP handles time in two formats: Epoch/Timestamp (numeric): Ideal for calculations. Formatted Datetime (string): Ideal for human readability and database storage. 6.1.1 getDateTime Generates the current time with high precision. Interface: getDateTime(format, timeDelta, timeZone, targetVar) Parameters format: Example: \"%Y-%m-%d %H:%M:%S\". If left empty, returns the current Epoch timestamp. timeDelta: Seconds to add (positive) or subtract (negative). Particularly useful for calculating token expiration times. timeZone: Time zone region (e.g., \"Europe/Madrid\"). 6.1.2 stampToDatetime Converts a numeric value (Unix Timestamp) into a human-readable string. Interface: stampToDatetime(timestamp, format, offset, targetVar) Common Use Case: Formatting dates retrieved from the database (Section V) before sending them to the client (Section II). 6.2 Advanced String Manipulation (replace / randomString) 6.2.1 replace Allows text cleaning and transformation. Essential when receiving client data that requires sanitization. Interface: replace(sourceText, oldText, newText, targetVar) Example Use Case: Removing spaces or unwanted characters from a username before executing a SQL query. 6.2.2 randomString Generates secure random alphanumeric strings. Interface: randomString(length, targetVar) Applications: Temporary password generation Session ID creation Unique file name generation 6.3 Security and Hash Operations (encodeSHA256) Although previously mentioned in the persistence section, this is fundamentally a data transformation utility. Mechanics Deterministic one-way function. AVAP uses an optimized implementation ensuring that the same input always produces the same hash. This enables secure login comparisons without storing or exposing the actual password. 6.4 The Return Command (return) Within functions and execution flows, return not only stops execution but can also inject the result of a subroutine back into the main flow. Complete Utility Flow Example // 1. Generate a temporary token randomString(16, token_raw) // 2. Calculate expiration (within 1 hour = 3600 seconds) getDateTime(\"%Y-%m-%d %H:%M:%S\", 3600, \"UTC\", expiration_date) // 3. Format a system message using Section I message = \"Your token %s expires on %s\" % (token_raw, expiration_date) // 4. Send to client (Section II) addResult(message) 6.5 Common Format Tokens (Cheat Sheet) Token Description Example %Y Full year 2026 %m Month (01–12) 02 %d Day (01–31) 23 %H Hour (00–23) 21 %M Minute (00–59) 45 Examples 1. Unix Timestamp Retrieval Code snippet getDateTime(\"\", 0, \"UTC\", now) addResult(now) 2. Database-Formatted Date Code snippet getDateTime(\"%Y-%m-%d %H:%M:%S\", 0, \"Europe/Madrid\", sql_date) addResult(sql_date) 3. Expiration Calculation (1 Day) Code snippet getDateTime(\"\", 86400, \"UTC\", expires_at) addResult(expires_at) 4. Timestamp to Readable Conversion Code snippet stampToDatetime(1708726162, \"%d/%m/%Y\", 0, human_date) addResult(human_date) 5. String Cleaning (Replace) Code snippet replace(\"REF_1234_OLD\", \"OLD\", \"NEW\", updated_ref) addResult(updated_ref) 6. Random Token Generator Code snippet randomString(32, security_token) addResult(security_token) 7. SHA256 Hash for Integrity Code snippet encodeSHA256(\"payload_data\", checksum) addResult(checksum)\n", + "\n", + "[3] id=chunk-3 source=16_Function_glossary.txt\n", + "Function Glossary randomString() The randomString() command generates a random string based on a specified pattern and stores it in a target variable. It is especially useful when random strings are needed to conform to a specific format, such as passwords or identifiers. Parameters Pattern Type: var Description: A regular expression (regex) pattern that defines the characters and structure of the string to be generated. It can be a direct value or a variable containing the pattern. For example, [a-zA-Z0-9] will generate a string that includes uppercase letters, lowercase letters, and numbers. Length Type: var Description: An integer value specifying the length of the random string to be generated. It can be a direct value or a variable containing the desired length. This value determines how many characters the resulting string will have. TargetVariable Type: var Description: The variable where the generated string will be stored. This variable should be used later in the program. Unlike the other parameters, this must be a variable and not a direct value. Usage Example // Direct call with values: randomString('[a-zA-Z0-9]', 8, generatedPassword) // Call using variables: pattern = '[a-zA-Z0-9]' length = 8 randomString(pattern, length, generatedPassword) stampToDatetime() The stampToDatetime() command converts a timestamp value to a date and time according to a specified format, applying a possible time difference, and stores the result in a target variable. It is useful for manipulating and formatting time values into different representations. Parameters timestamp Type: var Description: A value representing a timestamp, which can be provided directly or through a variable. This value is the starting point for conversion to a date and time format. Format Type: var Description: A format string that defines how the resulting date and time should be presented. This string follows the same conventions used in Python for formatting dates and times. Common symbols include: %Y: Year with four digits (e.g., 2024) %m: Month with two digits (01 to 12) %d: Day of the month with two digits (01 to 31) %H: Hour in 24-hour format (00 to 23) %M: Minutes (00 to 59) %S: Seconds (00 to 59) For example, the format %Y-%m-%d %H:%M:%S converts a timestamp into a string like 2024-08-25 14:30:00. It can be a direct value or a variable containing the desired format. TimeDelta Type: var Description: An optional value representing a time adjustment (positive or negative) applied to the timestamp before conversion. This value can be provided directly or through a variable and is expressed in seconds. TargetVariable Type: var Description: The variable where the resulting date and time from the conversion will be stored. Unlike the other parameters, this must be a variable and not a direct value. Usage Example // Direct call with values: stampToDatetime(1692966600, '%Y-%m-%d %H:%M:%S', 3600, convertedDatetime) // Call using variables: timestamp = 1692966600 format = '%Y-%m-%d %H:%M:%S' adjustment = 3600 stampToDatetime(timestamp, format, adjustment, convertedDatetime) In the first example, a timestamp is converted to a date and time in the format \"%Y-%m-%d %H:%M:%S\", applying a 3600-second (1-hour) adjustment, and the result is stored in the variable convertedDatetime. In the second example, variables are used to define the timestamp, format, and adjustment. getTimeStamp() The getTimeStamp() command converts a date and time string, given in a specific format, to a timestamp value. Additionally, it allows for an optional time adjustment before storing the result in a target variable. This command is useful for converting human-readable date and time representations to a numeric timestamp format, which can be used in calculations or time comparisons. Parameters DateString Type: var Description: A string representing a date and time. This string must follow the format specified in the Format parameter. It can be a direct value or a variable containing the date string. Format Type: var Description: A format string that defines how to interpret the date and time string (DateString). This string follows Python's conventions for formatting and parsing dates and times. Some common symbols include: %Y: Year with four digits (e.g., 2024) %m: Month with two digits (01 to 12) %d: Day of the month with two digits (01 to 31) %H: Hour in 24-hour format (00 to 23) %M: Minutes (00 to 59) %S: Seconds (00 to 59) For example, to interpret the string \"2024-08-25 14:30:00\", the format %Y-%m-%d %H:%M:%S would be used. It can be a direct value or a variable containing the format. TimeDelta Type: var Description: An optional value representing a time adjustment (positive or negative) applied to the timestamp after conversion. This value can be provided directly or through a variable and is expressed in seconds. TargetVariable Type: var Description: The variable where the resulting timestamp from the conversion will be stored. Unlike the other parameters, this must be a variable and not a direct value. Usage Example // Direct call with values: getTimeStamp('2024-08-25 14:30:00', '%Y-%m-%d %H:%M:%S', 3600, generatedTimestamp) // Call using variables: date = '2024-08-25 14:30:00' format = '%Y-%m-%d %H:%M:%S' adjustment = 3600 getTimeStamp(date, format, adjustment, generatedTimestamp) In the first example, the date and time string \"2024-08-25 14:30:00\" is converted to a timestamp, applying a 3600-second (1-hour) adjustment, and the result is stored in the variable generatedTimestamp. In the second example, variables are used to define the date, format, and adjustment. getRegex() The getRegex() command searches for matches in a source string using a regular expression (regex) pattern and stores the result in a target variable. This command is useful for extracting specific parts of a string that match a defined pattern, such as email addresses, phone numbers, or any other structure defined by a regex. Parameters SourceVariable Type: variable Description: The variable containing the source string in which to search for regex pattern matches. This string is the text on which the regex search will be applied. rePattern Type: variable Description: The variable containing the regular expression (regex) pattern that defines what to search for in the source string. This pattern should follow standard regex rules, allowing the specification of sequences of characters to identify in the source string. TargetVariable Type: variable Description: The variable where the search result will be stored. Depending on the context and the pattern used, the result could be the first match found, all matches, or even specific groups within the match. Usage Example // Direct call with values: sourceText = \"Email: user@example.com and phone: 123-456-7890\" pattern = r\"\\b\\d{3}-\\d{3}-\\d{4}\\b\" getRegex(sourceText, pattern, phoneNumber) // Call using variables: sourceText = \"Visit our website at https://www.example.com for more information.\" regexPattern = r\"https?://\\S+\" getRegex(sourceText, regexPattern, foundURL) In the first example, a phone number in the format 123-456-7890 is searched in the sourceText string and the result is stored in the phoneNumber variable. In the second example, a URL is extracted from the sourceText string using a regex that identifies URL patterns, and the result is stored in the foundURL variable. getDateTime() The getDateTime() command retrieves the current date and time, formats it according to a specified format, applies an optional time adjustment, and converts it to a specific time zone before storing the result in a target variable. It is useful for obtaining and manipulating the current date and time in different formats and time zones. Parameters Format Type: var Description: A format string that defines how the resulting date and time should be presented. This string follows the date and time formatting conventions used in Python. Some of the most common symbols include: %Y: Year with four digits (e.g., 2024) %m: Month with two digits (01 to 12) %d: Day of the month with two digits (01 to 31) %H: Hour in 24-hour format (00 to 23) %M: Minutes (00 to 59) %S: Seconds (00 to 59) For example, the format \"%Y-%m-%d %H:%M:%S\" will present the date and time as 2024-08-25 14:30:00. It can be a direct value or a variable containing the desired format. TimeDelta Type: var Description: An optional value representing a time adjustment (positive or negative) applied to the current date and time before conversion. This value can be provided directly or through a variable and is expressed in seconds. TimeZone Type: var Description: The time zone to which the date and time should be converted. This value can be a time zone identifier provided directly or through a variable. Some common time zones include: \"UTC\": Coordinated Universal Time \"America/New_York\": U.S. Eastern Time (EST/EDT) \"America/Los_Angeles\": U.S. Pacific Time (PST/PDT) \"Europe/London\": London Time (GMT/BST) \"Europe/Madrid\": Madrid Time (CET/CEST) \"Asia/Tokyo\": Tokyo Time (JST) \"Australia/Sydney\": Sydney Time (AEST/AEDT) You can use any time zone recognized by the pytz library in Python, which includes most time zones worldwide. TargetVariable Type: var Description: The variable in which the resulting date and time from the operation will be stored. Unlike the other parameters, this must be a variable and not a direct value. Usage Example // Direct call with values: getDateTime('%Y-%m-%d %H:%M:%S', 3600, 'UTC', currentTime) // Call using variables: format = '%Y-%m-%d %H:%M:%S' adjustment = 3600 timeZone = 'America/New_York' getDateTime(format, adjustment, timeZone, currentDateTime) In the first example, the current date and time are retrieved, adjusted by 3600 seconds (1 hour), converted to UTC, and stored in the variable currentTime. In the second example, variables are used to define the format, time adjustment, and time zone, with the result stored in the currentDateTime variable. encodeMD5() The encodeMD5() command generates an MD5 hash of the provided string and stores the result in a target variable. MD5 is a cryptographic hash function that produces a 128-bit value (32 hexadecimal characters), commonly used to verify data integrity. Parameters SourceVariable Type: var Description: The variable containing the text string to be encoded in MD5. It can be a direct value or a variable storing the input string. TargetVariable Type: var Description: The variable in which the resulting MD5 hash will be stored. Unlike the SourceVariable parameter, this must be a variable and not a direct value. Usage Example // Direct call with values: encodeMD5('example_string', md5Hash) // Call using variables: text = 'example_string' hashVariable = 'md5Hash' encodeMD5(text, hashVariable) In the first example, an MD5 hash is generated from the string 'example_string' and stored in the md5Hash variable. In the second example, a variable text is used to define the input string and another variable hashVariable is used to store the resulting MD5 hash. encodeSHA256() The encodeSHA256() command generates a SHA-256 hash of the provided string and stores the result in a target variable. SHA-256 is a cryptographic hash function that produces a 256-bit value (64 hexadecimal characters), offering greater security compared to MD5. Parameters SourceVariable Type: var Description: The variable containing the text string to be encoded in SHA-256. It can be a direct value or a variable storing the input string. TargetVariable Type: var Description: The variable in which the resulting SHA-256 hash will be stored. Unlike the SourceVariable parameter, this must be a variable and not a direct value. Usage Example // Direct call with values: encodeSHA256('example_string', sha256Hash) // Call using variables: text = 'example_string' hashVariable = 'sha256Hash' encodeSHA256(text, hashVariable) In the first example, a SHA-256 hash is generated from the string 'example_string' and stored in the sha256Hash variable. In the second example, a variable text is used to define the input string, and another variable hashVariable is used to store the resulting SHA-256 hash. getQueryParamList() The getQueryParamList() command extracts the query parameters from the current HTTP request and stores a list of these parameters in a target variable. This is useful for handling and processing query parameters in web applications. Parameters TargetVariable Type: var Description: The variable in which the extracted query parameter list will be stored. This should be a variable where the command's result will be saved. Command Flow Parameter Extraction: Accesses the query parameters from the current HTTP request. List Construction: Creates a list containing dictionaries, where each dictionary represents a query parameter and its associated value. Result Storage: Saves the list of parameters in the variable specified by TargetVariable. Usage Example Suppose the HTTP query has the following parameters: ?user=alice&age=30. // Define the variable to store the result queryParamsList = [] // Call the command to extract query parameters getQueryParamList(queryParamsList) // Return the list of query parameters via addResult addResult(queryParamsList) Given the query string ?user=alice&age=30, the getQueryParamList() command will generate the following list of parameters: [ {\"user\": \"alice\"}, {\"age\": \"30\"} ] getListLen() The getListLen() command calculates the length of a list and stores the result in a target variable. This command is useful for determining the number of elements in a list. Parameters SourceVariable Type: var Description: The variable containing the list whose length you want to calculate. It can be a variable that stores the list or a direct value representing the list. TargetVariable Type: var Description: The variable where the result of the list length will be stored. This should be a variable that will receive the integer value representing the number of elements in the list. Command Flow Retrieve the List: Access the list stored in the SourceVariable. Calculate the Length: Calculate the number of elements in the list. Store the Result: Save the calculated length in the variable specified by TargetVariable. Usage Example Suppose the list in myList is ['apple', 'banana', 'cherry']. // Variable definitions myList = ['apple', 'banana', 'cherry'] listLength = 0 // Call the command to calculate the length of the list getListLen(myList, listLength) // Return the list length through addResult addResult(listLength) Since the list myList has 3 elements, the getListLen() command will calculate that the length is 3. This value will be stored in the listLength variable and returned through addResult(listLength), resulting in the following output: 3 itemFromList() The itemFromList() command extracts a specific element from a list based on a given index and stores the result in a target variable. This is useful for accessing individual elements within a list. Parameters SourceVariable Type: var Description: The variable containing the list from which an element is to be extracted. It can be a variable that stores the list or a direct value representing the list. index Type: value Description: The index of the element to be extracted from the list. It must be an integer value that indicates the position of the element within the list. TargetVariable Type: var Description: The variable where the extracted element will be stored. It must be a variable that will receive the value of the element at the specified index position. Command Flow Access the List: Access the list stored in the SourceVariable. Extract the Element: Retrieve the element at the position specified by the index. Store the Result: Save the extracted element in the variable specified by TargetVariable. Usage Example Suppose the list in myList is ['apple', 'banana', 'cherry'] and you want to extract the element at index 1. // Variable definitions myList = ['apple', 'banana', 'cherry'] element = '' // Call the command to extract the element at index 1 itemFromList(myList, 1, element) // Return the extracted element through addResult addResult(element) Since index 1 corresponds to the element 'banana' in the myList, the itemFromList() command will extract 'banana' and store it in the variable element. The element variable will be returned through addResult(element), resulting in the following output: \"banana\" variableFromJSON() The variableFromJSON() command extracts the value associated with a specific key from a JSON object and stores the result in a target variable. This command is useful for accessing values within a JSON object. Parameters SourceVariable Type: var Description: The variable containing the JSON object from which a value is to be extracted. It can be a variable that stores the JSON object or a direct value representing the JSON object. key Type: value Description: The key whose value is to be extracted from the JSON object. It must be a value that represents the key within the JSON object. TargetVariable Type: var Description: The variable where the extracted value will be stored. It must be a variable that will receive the value associated with the specified key in the JSON object. Command Flow Access the JSON Object: Access the JSON object stored in the SourceVariable. Extract the Value: Retrieve the value associated with the key within the JSON object. Store the Result: Save the extracted value in the variable specified by TargetVariable. Usage Example Suppose the JSON object in jsonData is \"name\": \"Alice\", \"age\": 30 and you want to extract the value associated with the key \"name\". // Variable definitions jsonData = {\"name\": \"Alice\", \"age\": 30} nameValue = '' // Call the command to extract the value associated with the key \"name\" variableFromJSON(jsonData, \"name\", nameValue) // Return the extracted value through addResult addResult(nameValue) Since the value associated with the key \"name\" in the JSON object jsonData is \"Alice\", the variableFromJSON() command will extract \"Alice\" and store it in the variable nameValue. The nameValue variable will be returned through addResult(nameValue), resulting in the following output: \"Alice\" AddVariableToJSON() The AddVariableToJSON() command adds a new key and its corresponding value to a JSON object and stores the result in a target variable. This command is useful for updating a JSON object with new key-value pairs. Parameters Key Type: variable Description: The key to be added to the JSON object. It must be a variable that stores the key to be added. Value Type: variable Description: The value associated with the key to be added to the JSON object. It must be a variable that stores the corresponding value. TargetVariable Type: variable Description: The variable where the updated JSON object will be stored. It must be a variable that will receive the JSON object with the new key and its added value. Command Flow Access the JSON Object: Access the JSON object stored in the TargetVariable. Add the Key and Value: Add the new key and its associated value to the JSON object. Store the Result: Save the updated JSON object in the variable specified by TargetVariable. Usage Example Suppose the initial JSON object in jsonData is \"name\": \"Alice\", \"age\": 30, and you want to add a new key \"email\" with the value \"alice@example.com\". // Variable definitions jsonData = {\"name\": \"Alice\", \"age\": 30} newKey = \"email\" newValue = \"alice@example.com\" // Call the command to add the new key and value to the JSON object AddVariableToJSON(newKey, newValue, jsonData) // Return the updated JSON object through addResult addResult(jsonData) This updated JSON object will be stored in the variable jsonData and will be returned through addResult(jsonData), resulting in the following output: { \"name\": \"Alice\", \"age\": 30, \"email\": \"alice@example.com\" } variableToList() The variableToList() command converts an element into a list that contains only that element and stores the resulting list in a target variable. This command is useful to ensure that a single value is handled as a list in subsequent processing. Parameters element Type: variable Description: The variable that contains the element to be converted into a list. It can be any type of value that you want to include as the only item in the list. TargetVariable Type: variable Description: The variable in which the resulting list will be stored. It must be a variable that will receive the list with the included element. Command Flow Access the Element: Access the element stored in the element variable. Create the List: Create a list that contains only the provided element. Store the Result: Save the resulting list in the variable specified by TargetVariable. Usage Example Suppose the element in myElement is \"apple\" and you want to convert it into a list. // Variable definitions myElement = \"apple\" myList = [] // Call the command to convert the element into a list variableToList(myElement, myList) // Return the resulting list through addResult addResult(myList) Since myElement is \"apple\", the variableToList() command will convert this element into a list with a single item: [\"apple\"]. This list will be stored in the variable myList, and myList will be returned through addResult(myList), resulting in the following output: [\"apple\"] addParam() The addParam() command retrieves the value associated with a specific key from the query string of the current request and assigns this value to a target variable. This command is useful for extracting values from query parameters in an HTTP request and storing them in variables for processing. Parameters param Type: value Description: The key of the query string whose value you want to retrieve. It should be a value that represents the key in the query string. variable Type: var Description: The variable in which the retrieved value from the query string will be stored. It must be a variable that will receive the value associated with the specified key. Command Flow Retrieve the Value: Access the value associated with the param key from the query string of the current request. Assign the Value: Assign the retrieved value to the variable specified by variable. Usage Example Suppose the query string of the current request is ?user=alice&age=30, and you want to retrieve the value associated with the key \"user\". // Variable definitions userName = '' // Call the command to retrieve the value for the \"user\" key and assign it to the variable addParam(\"user\", userName) // Return the retrieved value through addResult addResult(userName) Given the query string ?user=alice&age=30, the addParam() command will retrieve the value \"alice\" associated with the key \"user\" and store it in the userName variable. The userName variable will be returned through addResult(userName), resulting in the following output: \"alice\" addResult() The addResult() command is used to return the content of a variable as part of the command or function response. It is the way to present results or processed data from commands and operations performed in the language. Parameters variable Type: var Description: The variable whose content is to be returned as the result. It should be a variable that contains the value or data you want to include in the response. Command Flow Access the Content: Access the content of the variable provided as a parameter. Return the Result: Include the content of the variable in the final response. Example Usage Suppose we have performed an operation and want to return the result stored in the result variable. // Define the variable with the result of an operation result = \"Operation completed successfully.\" // Call the command to return the content of the variable addResult(result) In this example, the addResult(result) command will return the content of the result variable, which is \"Operation completed successfully.\". This content will be presented as part of the response. Note The addResult() command is the primary mechanism for returning information and results in the language. Make sure that the variable passed to the command contains the desired data or result before calling addResult(). RequestPost() The RequestPost() command performs an HTTP POST request to a specified URL, sending a query string, headers, and a request body, and stores the result of the request in a destination variable. This command is useful for sending data to a server and handling the responses from the request. Parameters url Type: variable Description: The URL to which the POST request will be sent. It should be a variable containing the address of the resource to which the request is to be made. querystring Type: variable Description: The query string that will be appended to the URL. It should be a variable containing the query parameters in string format. headers Type: variable Description: The HTTP headers that will be included in the POST request. It should be a variable containing a dictionary of headers and their values. body Type: variable Description: The body of the POST request that will be sent to the server. It should be a variable containing the data to be sent in the request. o_result Type: variable Description: The variable in which the result of the POST request will be stored. It should be a variable that will receive the server's response. Command Flow Build the Request: Uses the provided URL, query string, headers, and body to construct the POST request. Send the Request: Sends the POST request to the specified server. Store the Result: Saves the server's response in the variable specified by o_result. Example Usage Suppose you want to send a POST request to https://api.example.com/data, with a query string userId=123, headers including Content-Type: application/json, and a body with JSON data. // Define variables url = \"https://api.example.com/data\" querystring = \"userId=123\" headers = {\"Content-Type\": \"application/json\"} body = '{\"name\": \"Alice\", \"age\": 30}' response = '' // Call the command to perform the POST request RequestPost(url, querystring, headers, body, response) // Return the request result via addResult addResult(response) In this example, the RequestPost() command will send a POST request to https://api.example.com/data with the provided query string, headers, and body. The server's response will be stored in the response variable, and this variable will be returned via addResult(response). The result of the request will be included in the final response. ormCreateTable() The ormCreateTable() command creates a new table in a database using the specified ORM (Object-Relational Mapping). This command defines the columns of the table and their data types, and stores a reference to the created table in a destination variable. Parameters fields Type: value Description: A string containing the names of the table columns, separated by commas. Each column name should correspond to a field in the table. fieldsType Type: value Description: A string containing the data types for each column, separated by commas. The data types should be in the same order as the column names in fields. dbaseName Type: value Description: The name of the database where the table will be created. It should be a string indicating the target database. varTarget Type: variable Description: The variable in which the reference to the created table will be stored. It should be a variable that will receive the reference to the new table. Command Flow Define the Table: Uses the column names (fields) and their data types (fieldsType) to define the structure of the new table. Create the Table: Creates the table in the database specified by dbaseName using the provided definition. Store the Result: Saves the reference to the created table in the variable specified by varTarget. Example Usage Suppose you want to create a table called users in a database called myDatabase, with two columns: username of type VARCHAR and age of type INTEGER. // Define variables fields = \"username,age\" fieldsType = \"VARCHAR,INTEGER\" dbaseName = \"myDatabase\" tableReference = '' // Call the command to create the table ormCreateTable(fields, fieldsType, dbaseName, tableReference) // Return the reference to the created table via addResult addResult(tableReference) In this example, the ormCreateTable() command will create a table in the myDatabase database with the specified columns and data types. The reference to the new table will be stored in the tableReference variable, and this variable will be returned via addResult(tableReference). The output will include the reference to the created table. ormCheckTable() The ormCheckTable() command checks for the existence of a table in a specific database and stores the result in a destination variable. This command is useful for verifying if a table already exists before attempting further operations on it. Parameters dbaseName Type: value Description: The name of the database in which the table's existence should be checked. It should be a string indicating the database to check. varTarget Type: variable Description: The variable in which the result of the check will be stored. It should be a variable that will receive a value indicating whether the table exists or not. Command Flow Check Existence: Accesses the database specified by dbaseName to verify if the requested table exists. Store the Result: Saves the result of the check in the variable specified by varTarget. The stored value will indicate whether the table exists (True or False). Example Usage Suppose you want to check if a table called users exists in a database called myDatabase. // Define variables dbaseName = \"myDatabase\" tableExists = '' // Call the command to check the existence of the table ormCheckTable(dbaseName, tableExists) // Return the result of the check via addResult addResult(tableExists) In this example, the ormCheckTable() command will check for the existence of the users table in the myDatabase database. The result of the check (whether the table exists or not) will be stored in the tableExists variable, and this variable will be returned via addResult(tableExists). The output will reflect whether the table exists (True) or not (False). ormAccessUpdate() The ormAccessUpdate() command updates records in a database table based on the provided selection criteria. This command modifies the values of specified fields in a database using the corresponding values from variables. Parameters fields Type: variable Description: A string containing the names of the fields to be updated. The field names should be separated by commas. fieldsValuesVariables Type: variable Description: A string containing the names of the variables holding the new values for the specified fields. The variable names should be separated by commas, in the same order as the fields in fields. dbase Type: variable Description: The name of the database where the table to be updated is located. It should be a variable containing the name of the database. selector Type: variable Description: A condition to select the records to be updated. It should be a string specifying the selection criteria in SQL format, such as id = 1. varTarget Type: variable Description: The variable in which the result of the update operation will be stored. It should be a variable that will receive a value indicating whether the update was successful or not. Command Flow Define Fields and Values: Uses the field names (fields) and the variables with the values to be updated (fieldsValuesVariables) to define which records should be modified and with what data. Select Records: Uses the condition provided in selector to identify the records to be updated. Update the Database: Performs the update in the database specified by dbase, applying the changes to the records that meet the selector condition. Store the Result: Saves the result of the update operation in the variable specified by varTarget. The stored value will indicate whether the update was successful (True) or failed (False). Example Usage Suppose you want to update the age field to 31 for the user with id equal to 1 in a database called myDatabase. // Define variables fields = \"age\" fieldsValuesVariables = \"newAge\" dbase = \"myDatabase\" selector = \"id = 1\" updateSuccess = '' // Define the variable holding the new value newAge = 31 // Call the command to update the record ormAccessUpdate(fields, fieldsValuesVariables, dbase, selector, updateSuccess) // Return the result of the update via addResult addResult(updateSuccess) In this example, the ormAccessUpdate() command will update the age field in the myDatabase database for the record where id = 1. The new value for age is 31, stored in the newAge variable. The updateSuccess variable will store the result of the operation (whether it was successful or not), and this variable will be returned via addResult(updateSuccess). ormAccessSelect() The ormAccessSelect() command retrieves records from a table in a database based on the provided selection criteria. This command selects the desired fields and stores the results in a target variable. Parameters fields Type: variable Description: A string containing the names of the fields to be retrieved. The field names should be separated by commas. dbase Type: variable Description: The name of the database from which records should be retrieved. It must be a variable containing the name of the database. selector Type: variable Description: A condition to select the records to be retrieved. It must be a string specifying the selection criteria in SQL format, such as id = 1. varTarget Type: variable Description: The variable in which the query results will be stored. It must be a variable that will receive a list of dictionaries, each representing a retrieved record. Command Flow Defining the Fields: Use the field names (fields) to specify which data should be retrieved. Selecting Records: Use the condition provided in selector to identify which records should be selected from the database. Retrieving Data: Access the database specified by dbase and retrieve the records that meet the selector condition, including only the specified fields. Storing the Result: Save the query results in the variable specified by varTarget. The stored value will be a list of dictionaries, where each dictionary represents a retrieved record with the requested fields. Example Usage Suppose you want to retrieve the username field for all users where age is greater than 25 from a database called myDatabase. // Define variables fields = \"username\" dbase = \"myDatabase\" selector = \"age > 25\" usersList = '' // Call the command to retrieve the records ormAccessSelect(fields, dbase, selector, usersList) // Return the query results via addResult addResult(usersList) In this example, the ormAccessSelect() command will retrieve the username field for all users in the myDatabase database where age is greater than 25. The results will be stored in the usersList variable, and this variable will be returned via addResult(usersList). The output will be a list of dictionaries, each representing a user whose username has been retrieved. ormAccessInsert() The ormAccessInsert() command inserts a new record into a database table using the provided values for the fields. This command defines the fields and their corresponding values, and stores the result of the operation in a target variable. Parameters fields Type: variable Description: A string containing the names of the fields into which the values will be inserted. The field names should be separated by commas. fieldsValuesVariables Type: variable Description: A string containing the names of the variables that hold the values to be inserted into the specified fields. The variable names should be separated by commas, in the same order as the fields in fields. dbase Type: variable Description: The name of the database where the table into which the new record should be inserted is located. It must be a variable containing the name of the database. varTarget Type: variable Description: The variable in which the result of the insertion operation will be stored. It must be a variable that will receive a value indicating whether the insertion was successful or not. Command Flow Defining the Fields and Values: Use the field names (fields) and the variables with the values to be inserted (fieldsValuesVariables) to define what data should be inserted. Inserting into the Database: Perform the insertion of the new record into the database specified by dbase, using the provided values. Storing the Result: Save the result of the insertion operation in the variable specified by varTarget. The stored value will indicate whether the insertion was successful (True) or failed (False). Example Usage Suppose you want to insert a new record into a table called users in a database called myDatabase, with values for username and age coming from the variables newUsername and newAge. // Define variables fields = \"username,age\" fieldsValuesVariables = \"newUsername,newAge\" dbase = \"myDatabase\" insertSuccess = '' // Define the variables with the new values newUsername = \"Alice\" newAge = 31 // Call the command to insert the new record ormAccessInsert(fields, fieldsValuesVariables, dbase, insertSuccess) // Return the result of the insertion via addResult addResult(insertSuccess) In this example, the ormAccessInsert() command will insert a new record into the myDatabase database in the users table. The values for username and age are provided by the newUsername and newAge variables. The insertSuccess variable will store the result of the operation (whether it was successful or not), and this variable will be returned via addResult(insertSuccess). The output will reflect whether the insertion was successful (True) or failed (False). ormAI() The ormAI() command uses an artificial intelligence model to convert a natural language query into an SQL statement, which is then executed against a database. This command processes a natural language query to generate an SQL statement that is executed on the table specified in the source parameter, and stores the result in a target variable. Parameters prompt Type: variable Description: A string in natural language that describes the query to be made. For example, \"get the value of the row with id 5\". source Type: variable Description: The name of the table on which the generated query should be executed. It must be a variable containing the name of the table in the database. TargetVariable Type: variable Description: The variable in which the result of the query will be stored. It must be a variable that will receive the result of the generated and executed SQL query. Command Flow Generating SQL Query: Use the artificial intelligence model to convert the prompt into an SQL statement. For example, if the prompt is \"get the value of the row with id 5\", the AI will generate the SQL query SELECT * FROM source WHERE id = 5;. Executing the Query: Execute the generated SQL statement on the table specified in source. Storing the Result: Save the result of the query execution in the variable specified by TargetVariable. The result will be the dataset retrieved by the executed SQL statement. Example Usage Suppose you want to retrieve all the data from the row with id equal to 5 from a table called users. // Define variables prompt = \"get the value of the row with id 5\" source = \"users\" queryResult = '' // Call the command to process the query ormAI(prompt, source, queryResult) // Return the query result via addResult addResult(queryResult) In this example, the ormAI() command will convert the prompt into an SQL query: SELECT * FROM users WHERE id = 5;. This query will be executed on the users table, and the results will be stored in the queryResult variable. The queryResult variable will be returned via addResult(queryResult). The output will be the dataset retrieved by the executed SQL statement. functionAI() The functionAI() command uses an artificial intelligence model to convert a natural language description of a function or process into a code implementation, which is then executed and returns the result. This command converts a description provided in prompt into a function that operates on the data of the table specified in source, and stores the result in a target variable. Parameters prompt Type: variable Description: A string in natural language that describes the process or function to be executed. For example, \"calculate the average of the salary column\". source Type: variable Description: The name of the table on which the generated function should be executed. It must be a variable containing the name of the table in the database. TargetVariable Type: variable Description: The variable in which the result of the executed function or process will be stored. It must be a variable that will receive the result of the generated and executed code. Command Flow Generating Code: Use the artificial intelligence model to convert the prompt into a code implementation. For example, if the prompt is \"calculate the average of the salary column\", the AI will generate the code necessary to calculate the average of that column. Executing the Code: Execute the generated code on the table specified in source. Storing the Result: Save the result of the code execution in the variable specified by TargetVariable. The result will be the calculated value or the dataset produced by the executed code. Example Usage Suppose you want to calculate the average of the salary column in a table called employees. // Define variables prompt = \"calculate the average of the salary column\" source = \"employees\" averageSalary = '' // Call the command to process the function functionAI(prompt, source, averageSalary) // Return the result of the function via addResult addResult(averageSalary) In this example, the functionAI() command will convert the prompt into a code implementation to calculate the average of the salary column in the employees table. The result of the calculation will be stored in the averageSalary variable, and this variable will be returned via addResult(averageSalary). The output will be the calculated average of the salary column.\n", + "\n", + "[4] id=chunk-4 source=24_Master_example.txt\n", + "Master Example (Combining Sections) This example shows how a real flow uses almost all sections: // SECTION I & II: Registration and Input registerEndpoint(\"/v1/user\", \"POST\", [], \"Create User\", main, final_res) function main(){ addParam(\"user\", u) addParam(\"pass\", p) // SECTION III & VI: Validation and Security if(u, None, \"==\") addVar(_status, 400) return(\"User is required\") end() encodeSHA256(p, pass_hash) // SECTION IV: Asynchrony (Audit log) go_async(\"audit\") ormDirect(\"INSERT INTO audit (event) VALUES ('User creation attempt')\", r) end() // SECTION V: Persistence db = avapConnector(\"TOKEN_DB\") res_db = db.query(\"INSERT INTO users (name, pass) VALUES ('%s', '%s')\" % (u, pass_hash)) // SECTION II: Response addVar(_status, 201) addResult(res_db) return(res_db) }\n", + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "Suppose you want to create a table called users in a database called myDatabase, with two columns: username of type VARCHAR and age of type INTEGER. How would you do that in AVAP?\n" + ] + }, + { + "ename": "TypeError", + "evalue": "generate() missing 1 required positional argument: 'state'", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mTypeError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[63]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m a = \u001b[43mstream_graph_updates\u001b[49m\u001b[43m(\u001b[49m\u001b[43muser_input\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mguided_graph\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[61]\u001b[39m\u001b[32m, line 7\u001b[39m, in \u001b[36mstream_graph_updates\u001b[39m\u001b[34m(user_input, graph)\u001b[39m\n\u001b[32m 6\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mstream_graph_updates\u001b[39m(user_input: \u001b[38;5;28mstr\u001b[39m, graph: StateGraph):\n\u001b[32m----> \u001b[39m\u001b[32m7\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mevent\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mgraph\u001b[49m\u001b[43m.\u001b[49m\u001b[43mstream\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 8\u001b[39m \u001b[43m \u001b[49m\u001b[43m{\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mmessages\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[43m{\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mrole\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43muser\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mcontent\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43muser_input\u001b[49m\u001b[43m}\u001b[49m\u001b[43m]\u001b[49m\u001b[43m}\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 9\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;66;43;03m#config=config,\u001b[39;49;00m\n\u001b[32m 10\u001b[39m \u001b[43m \u001b[49m\u001b[43mstream_mode\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mvalues\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 11\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m:\u001b[49m\n\u001b[32m 12\u001b[39m \u001b[43m \u001b[49m\u001b[43mevent\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mmessages\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m[\u001b[49m\u001b[43m-\u001b[49m\u001b[32;43m1\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m.\u001b[49m\u001b[43mpretty_print\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/VsCodeProjects/assistance-engine/.venv/lib/python3.12/site-packages/langgraph/pregel/main.py:2646\u001b[39m, in \u001b[36mPregel.stream\u001b[39m\u001b[34m(self, input, config, context, stream_mode, print_mode, output_keys, interrupt_before, interrupt_after, durability, subgraphs, debug, **kwargs)\u001b[39m\n\u001b[32m 2644\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m task \u001b[38;5;129;01min\u001b[39;00m loop.match_cached_writes():\n\u001b[32m 2645\u001b[39m loop.output_writes(task.id, task.writes, cached=\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[32m-> \u001b[39m\u001b[32m2646\u001b[39m \u001b[43m\u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43m_\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mrunner\u001b[49m\u001b[43m.\u001b[49m\u001b[43mtick\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 2647\u001b[39m \u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[43mt\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mt\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mloop\u001b[49m\u001b[43m.\u001b[49m\u001b[43mtasks\u001b[49m\u001b[43m.\u001b[49m\u001b[43mvalues\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;129;43;01mnot\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mt\u001b[49m\u001b[43m.\u001b[49m\u001b[43mwrites\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 2648\u001b[39m \u001b[43m \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mstep_timeout\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 2649\u001b[39m \u001b[43m \u001b[49m\u001b[43mget_waiter\u001b[49m\u001b[43m=\u001b[49m\u001b[43mget_waiter\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 2650\u001b[39m \u001b[43m \u001b[49m\u001b[43mschedule_task\u001b[49m\u001b[43m=\u001b[49m\u001b[43mloop\u001b[49m\u001b[43m.\u001b[49m\u001b[43maccept_push\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 2651\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\u001b[43m:\u001b[49m\n\u001b[32m 2652\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;66;43;03m# emit output\u001b[39;49;00m\n\u001b[32m 2653\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43;01myield from\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43m_output\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 2654\u001b[39m \u001b[43m \u001b[49m\u001b[43mstream_mode\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mprint_mode\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43msubgraphs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstream\u001b[49m\u001b[43m.\u001b[49m\u001b[43mget\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mqueue\u001b[49m\u001b[43m.\u001b[49m\u001b[43mEmpty\u001b[49m\n\u001b[32m 2655\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 2656\u001b[39m loop.after_tick()\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/VsCodeProjects/assistance-engine/.venv/lib/python3.12/site-packages/langgraph/pregel/_runner.py:167\u001b[39m, in \u001b[36mPregelRunner.tick\u001b[39m\u001b[34m(self, tasks, reraise, timeout, retry_policy, get_waiter, schedule_task)\u001b[39m\n\u001b[32m 165\u001b[39m t = tasks[\u001b[32m0\u001b[39m]\n\u001b[32m 166\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m167\u001b[39m \u001b[43mrun_with_retry\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 168\u001b[39m \u001b[43m \u001b[49m\u001b[43mt\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 169\u001b[39m \u001b[43m \u001b[49m\u001b[43mretry_policy\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 170\u001b[39m \u001b[43m \u001b[49m\u001b[43mconfigurable\u001b[49m\u001b[43m=\u001b[49m\u001b[43m{\u001b[49m\n\u001b[32m 171\u001b[39m \u001b[43m \u001b[49m\u001b[43mCONFIG_KEY_CALL\u001b[49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mpartial\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 172\u001b[39m \u001b[43m \u001b[49m\u001b[43m_call\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 173\u001b[39m \u001b[43m \u001b[49m\u001b[43mweakref\u001b[49m\u001b[43m.\u001b[49m\u001b[43mref\u001b[49m\u001b[43m(\u001b[49m\u001b[43mt\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 174\u001b[39m \u001b[43m \u001b[49m\u001b[43mretry_policy\u001b[49m\u001b[43m=\u001b[49m\u001b[43mretry_policy\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 175\u001b[39m \u001b[43m \u001b[49m\u001b[43mfutures\u001b[49m\u001b[43m=\u001b[49m\u001b[43mweakref\u001b[49m\u001b[43m.\u001b[49m\u001b[43mref\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfutures\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 176\u001b[39m \u001b[43m \u001b[49m\u001b[43mschedule_task\u001b[49m\u001b[43m=\u001b[49m\u001b[43mschedule_task\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 177\u001b[39m \u001b[43m \u001b[49m\u001b[43msubmit\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43msubmit\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 178\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 179\u001b[39m \u001b[43m \u001b[49m\u001b[43m}\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 180\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 181\u001b[39m \u001b[38;5;28mself\u001b[39m.commit(t, \u001b[38;5;28;01mNone\u001b[39;00m)\n\u001b[32m 182\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m exc:\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/VsCodeProjects/assistance-engine/.venv/lib/python3.12/site-packages/langgraph/pregel/_retry.py:42\u001b[39m, in \u001b[36mrun_with_retry\u001b[39m\u001b[34m(task, retry_policy, configurable)\u001b[39m\n\u001b[32m 40\u001b[39m task.writes.clear()\n\u001b[32m 41\u001b[39m \u001b[38;5;66;03m# run the task\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m42\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mtask\u001b[49m\u001b[43m.\u001b[49m\u001b[43mproc\u001b[49m\u001b[43m.\u001b[49m\u001b[43minvoke\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtask\u001b[49m\u001b[43m.\u001b[49m\u001b[43minput\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 43\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m ParentCommand \u001b[38;5;28;01mas\u001b[39;00m exc:\n\u001b[32m 44\u001b[39m ns: \u001b[38;5;28mstr\u001b[39m = config[CONF][CONFIG_KEY_CHECKPOINT_NS]\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/VsCodeProjects/assistance-engine/.venv/lib/python3.12/site-packages/langgraph/_internal/_runnable.py:656\u001b[39m, in \u001b[36mRunnableSeq.invoke\u001b[39m\u001b[34m(self, input, config, **kwargs)\u001b[39m\n\u001b[32m 654\u001b[39m \u001b[38;5;66;03m# run in context\u001b[39;00m\n\u001b[32m 655\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m set_config_context(config, run) \u001b[38;5;28;01mas\u001b[39;00m context:\n\u001b[32m--> \u001b[39m\u001b[32m656\u001b[39m \u001b[38;5;28minput\u001b[39m = \u001b[43mcontext\u001b[49m\u001b[43m.\u001b[49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\u001b[43mstep\u001b[49m\u001b[43m.\u001b[49m\u001b[43minvoke\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 657\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 658\u001b[39m \u001b[38;5;28minput\u001b[39m = step.invoke(\u001b[38;5;28minput\u001b[39m, config)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/VsCodeProjects/assistance-engine/.venv/lib/python3.12/site-packages/langgraph/_internal/_runnable.py:400\u001b[39m, in \u001b[36mRunnableCallable.invoke\u001b[39m\u001b[34m(self, input, config, **kwargs)\u001b[39m\n\u001b[32m 398\u001b[39m run_manager.on_chain_end(ret)\n\u001b[32m 399\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m400\u001b[39m ret = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 401\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.recurse \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(ret, Runnable):\n\u001b[32m 402\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m ret.invoke(\u001b[38;5;28minput\u001b[39m, config)\n", + "\u001b[31mTypeError\u001b[39m: generate() missing 1 required positional argument: 'state'", + "During task with name 'generate' and id 'c4306dab-0608-b459-1e48-2e3f8bf6fec2'" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "ename": "", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[1;31mThe Kernel crashed while executing code in the current cell or a previous cell. \n", + "\u001b[1;31mPlease review the code in the cell(s) to identify a possible cause of the failure. \n", + "\u001b[1;31mClick here for more info. \n", + "\u001b[1;31mView Jupyter log for further details." + ] + } + ], + "source": [ + "a = stream_graph_updates(user_input, guided_graph)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "assistance-engine", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/research/agents/n00 LangGrah Tool Calling Agent.ipynb b/research/agents/n00 LangGrah Tool Calling Agent.ipynb new file mode 100644 index 0000000..d11adc5 --- /dev/null +++ b/research/agents/n00 LangGrah Tool Calling Agent.ipynb @@ -0,0 +1,1021 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9f97dd1e", + "metadata": {}, + "source": [ + "# Libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "9e974df6", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import sys\n", + "from pathlib import Path\n", + "from typing import TypedDict, List, Optional, Annotated, Literal\n", + "from IPython.display import Image, display\n", + "from pydantic import BaseModel, Field\n", + "import pandas as pd\n", + "from pathlib import Path\n", + "from langchain_core.messages import HumanMessage\n", + "\n", + "# Ensure the project root is on the path so `src` is importable\n", + "_project_root = str(Path(__file__).resolve().parents[2]) if \"__file__\" in dir() else str(Path.cwd().parents[1])\n", + "if _project_root not in sys.path:\n", + " sys.path.insert(0, _project_root)\n", + "\n", + "from langchain_core.documents import Document\n", + "from langchain_core.messages import BaseMessage, SystemMessage, AIMessage\n", + "from langchain_core.tools import tool\n", + "from langgraph.checkpoint.memory import InMemorySaver\n", + "from langgraph.graph.message import add_messages\n", + "from langchain_ollama import ChatOllama, OllamaEmbeddings\n", + "from langchain_elasticsearch import ElasticsearchStore\n", + "from langgraph.graph import StateGraph, END\n", + "from langgraph.prebuilt import ToolNode, tools_condition\n", + "\n", + "from src.utils.llm_factory import create_chat_model\n", + "from src.utils.emb_factory import create_embedding_model\n", + "from src.config import (\n", + " ELASTICSEARCH_LOCAL_URL,\n", + " ELASTICSEARCH_INDEX,\n", + " OLLAMA_MODEL_NAME,\n", + " OLLAMA_EMB_MODEL_NAME,\n", + " DOCS_DIR,\n", + " DATA_DIR\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30edcecc", + "metadata": {}, + "outputs": [], + "source": [ + "llm = create_chat_model(\n", + " provider=\"ollama\",\n", + " model=OLLAMA_MODEL_NAME,\n", + " temperature=0,\n", + " validate_model_on_init=True,\n", + ")\n", + "embeddings = create_embedding_model(\n", + " provider=\"ollama\",\n", + " model=OLLAMA_EMB_MODEL_NAME,\n", + ")\n", + "vector_store = ElasticsearchStore(\n", + " es_url=ELASTICSEARCH_LOCAL_URL,\n", + " index_name=ELASTICSEARCH_INDEX,\n", + " embedding=embeddings,\n", + " query_field=\"text\",\n", + " vector_query_field=\"vector\",\n", + " # strategy=ElasticsearchStore.ApproxRetrievalStrategy(\n", + " # hybrid=True,\n", + " # rrf={\"rank_constant\": 60, \"window_size\": 100}\n", + " # )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "873ea2f6", + "metadata": {}, + "source": [ + "### State" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5f8c88cf", + "metadata": {}, + "outputs": [], + "source": [ + "class AgentState(TypedDict):\n", + " messages: Annotated[list, add_messages]\n", + " reformulated_query: str\n", + " context: str" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "fd8ed542", + "metadata": {}, + "outputs": [], + "source": [ + "class AgenticAgentState(TypedDict):\n", + " messages: Annotated[list, add_messages]" + ] + }, + { + "cell_type": "markdown", + "id": "1d60c120", + "metadata": {}, + "source": [ + "### Tools" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "f0a21230", + "metadata": {}, + "outputs": [], + "source": [ + "retrieve_kwargs = {\"k\": 3}" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "f9359747", + "metadata": {}, + "outputs": [], + "source": [ + "def format_context(docs: List[Document]) -> str:\n", + " chunks: List[str] = []\n", + " for i, doc in enumerate(docs, 1):\n", + " source = (doc.metadata or {}).get(\"source\", \"Untitled\")\n", + " source_id = (doc.metadata or {}).get(\"id\", f\"chunk-{i}\")\n", + " text = doc.page_content or \"\"\n", + " chunks.append(f\"[{i}] id={source_id} source={source}\\n{text}\")\n", + " return \"\\n\\n\".join(chunks)\n", + "\n", + "\n", + "@tool\n", + "def context_retrieve(query: str) -> str:\n", + " \"\"\"Consults vector store to respond AVAP related questions\n", + " Args:\n", + " query (str): The input query for which to retrieve relevant documents.\n", + " \"\"\"\n", + " retriever = vector_store.as_retriever(\n", + " search_type=\"similarity\",\n", + " search_kwargs=retrieve_kwargs,\n", + " )\n", + " docs = retriever.invoke(query)\n", + " return format_context(docs)" + ] + }, + { + "cell_type": "markdown", + "id": "395966e2", + "metadata": {}, + "source": [ + "### Agent" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "66ae23f0", + "metadata": {}, + "outputs": [], + "source": [ + "REFORMULATE_PROMPT = SystemMessage(\n", + " content=(\n", + " \"You are a deterministic query rewriting function.\\n\"\n", + " \"You convert natural language questions into keyword search queries.\\n\\n\"\n", + " \"Strict constraints:\\n\"\n", + " \"1. Keep function names and technical tokens unchanged.\\n\"\n", + " \"2. Remove filler phrases.\\n\"\n", + " \"3. Do not answer.\\n\"\n", + " \"4. Do not explain.\\n\"\n", + " \"5. Do not generate code.\\n\"\n", + " \"6. Return a single-line query only.\\n\"\n", + " \"7. If already optimal, return unchanged.\\n\"\n", + " )\n", + ")\n", + "\n", + "GENERATE_PROMPT = SystemMessage(\n", + " content=\"\"\"You are an agent designed to assist users with AVAP (Advanced Virtual API Programming) language.\n", + " It's a new language, so you should know nothing about it.\n", + " Use ONLY the provided context to answer AVAP-related questions.\n", + " If the context does not contain enough information, say so honestly.\n", + " If the question is not related to AVAP, answer based on your general knowledge.\n", + "\n", + " Context:\n", + " {context}\"\"\"\n", + ")\n", + "\n", + "AGENTIC_PROMPT = SystemMessage(\n", + " content=\"\"\"You are an agent designed to assist users with AVAP (Advanced Virtual API Programming) language.\n", + " It's a new language, so you should know nothing about it.\n", + " Use ONLY the provided 'context_retrieve' tool to answer AVAP-related questions.\n", + " The 'context_retrieve' tool receives a user query (as a string) and returns relevant context from a vector store.\n", + " If the context does not contain enough information, say so honestly.\n", + " If the question is not related to AVAP, answer based on your general knowledge.\n", + " \"\"\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "36d0f54e", + "metadata": {}, + "outputs": [], + "source": [ + "def reformulate(state: AgentState) -> AgentState:\n", + " \"\"\"Use the LLM to rewrite the user query for better retrieval.\"\"\"\n", + " user_msg = state[\"messages\"][-1]\n", + " resp = llm.invoke([REFORMULATE_PROMPT, user_msg])\n", + " reformulated = resp.content.strip()\n", + " print(f\"[reformulate] '{user_msg.content}' → '{reformulated}'\")\n", + " return {\"reformulated_query\": reformulated}\n", + "\n", + "\n", + "def retrieve(state: AgentState) -> AgentState:\n", + " \"\"\"Retrieve context using the reformulated query.\"\"\"\n", + " query = state[\"reformulated_query\"]\n", + " docs = vector_store.as_retriever(\n", + " search_type=\"similarity\",\n", + " search_kwargs=retrieve_kwargs,\n", + " ).invoke(query)\n", + " context = format_context(docs)\n", + " print(f\"[retrieve] {len(docs)} docs fetched\")\n", + " print(context)\n", + " return {\"context\": context}\n", + "\n", + "\n", + "def generate(state: AgentState) -> AgentState:\n", + " \"\"\"Generate the final answer using retrieved context.\"\"\"\n", + " prompt = SystemMessage(\n", + " content=GENERATE_PROMPT.content.format(context=state[\"context\"])\n", + " )\n", + " resp = llm.invoke([prompt] + state[\"messages\"])\n", + " return {\"messages\": [resp]}" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "f073edc9", + "metadata": {}, + "outputs": [], + "source": [ + "def agent(state: AgentState) -> AgentState:\n", + " llm_with_tools = llm.bind_tools(tools)\n", + " return {\"messages\": [llm_with_tools.invoke([SystemMessage(content=AGENTIC_PROMPT.content)] + state[\"messages\"])]}" + ] + }, + { + "cell_type": "markdown", + "id": "ef55bca3", + "metadata": {}, + "source": [ + "### Graph" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "f7a0993f", + "metadata": {}, + "outputs": [], + "source": [ + "tools = [context_retrieve]\n", + "tool_node = ToolNode(tools=tools)\n", + "memory = InMemorySaver()\n", + "\n", + "graph_builder = StateGraph(AgenticAgentState)\n", + "\n", + "graph_builder.add_node(\"agent\", agent)\n", + "graph_builder.add_node(\"tools\", tool_node)\n", + "\n", + "graph_builder.set_entry_point(\"agent\")\n", + "graph_builder.add_conditional_edges(\n", + " \"agent\",\n", + " tools_condition,\n", + ")\n", + "graph_builder.add_edge(\"tools\", \"agent\")\n", + "\n", + "agentic_graph = graph_builder.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "2fec3fdb", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "try:\n", + " display(Image(agentic_graph.get_graph().draw_mermaid_png()))\n", + "except Exception:\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "1e9aff05", + "metadata": {}, + "source": [ + "### Test" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "8569cf39", + "metadata": {}, + "outputs": [], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"5\"}, \n", + " #\"callbacks\": [langfuse_handler],\n", + " #\"run_name\": \"rag-local-test\"differences between getDatetime() and getTimeStamp() functions in AVAP\n", + " }\n", + "\n", + "def stream_graph_updates(user_input: str, graph: StateGraph):\n", + " for event in graph.stream(\n", + " {\"messages\": [{\"role\": \"user\", \"content\": user_input}]},\n", + " #config=config,\n", + " stream_mode=\"values\",\n", + " ):\n", + " event[\"messages\"][-1].pretty_print()\n", + " # last_msg = event[\"messages\"][-1]\n", + " # if isinstance(last_msg, AIMessage):\n", + " # last_msg.pretty_print()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "a1a1f3cf", + "metadata": {}, + "outputs": [], + "source": [ + "user_input = \"\"\"Suppose you want to create a table called users in a database called myDatabase, with two columns: username of type VARCHAR and age of type INTEGER. How would you do that in AVAP?\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "afa0c11f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Found 32 .avap files\n", + "\n", + "Processing: asignacion_booleana.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: asignacion_matematica.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: bucle_1_10.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: bucle_longitud_de_datos.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: calculo_de_expiracion.avap...\n", + " ✗ Error: failed to parse JSON: unexpected end of JSON input (status code: -1)\n", + "\n", + "Processing: captura_de_id.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: captura_de_listas_multiples.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: comparacion_simple.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: concatenacion_dinamica.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: construccion_dinamica_de_objeto.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: contador_de_parametros.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: conversion_timestamp_legible.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: else_estandar.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: expresion_compleja.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: fecha_para_base_de_datos.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: funcion_de_suma.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: funcion_validacion_acceso.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: generador_de_tokens_aleatorios.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: hash_SHA256_para_integridad.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: hola_mundo.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: if_desigualdad.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: limpieza_de_strings.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: manejo_error_sql_critico.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: obtencion_timestamp.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: ormAccessCreate.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: paginacion_dinamica_recursos.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: referencia_por_valor.avap...\n", + " ✗ Error: failed to parse JSON: unexpected end of JSON input (status code: -1)\n", + "\n", + "Processing: respuesta_multiple.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: salida_bucle_correcta.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: try_catch_request.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: validacion_de_nulo.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "Processing: validacion_in_pertenece_a_lista.avap...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✓ Processed successfully\n", + "\n", + "\n", + "Completed! Generated 32 results.\n", + "\n", + " filename \\\n", + "0 asignacion_booleana.avap \n", + "1 asignacion_matematica.avap \n", + "2 bucle_1_10.avap \n", + "3 bucle_longitud_de_datos.avap \n", + "4 calculo_de_expiracion.avap \n", + "5 captura_de_id.avap \n", + "6 captura_de_listas_multiples.avap \n", + "7 comparacion_simple.avap \n", + "8 concatenacion_dinamica.avap \n", + "9 construccion_dinamica_de_objeto.avap \n", + "\n", + " answer \\\n", + "0 # Understanding AVAP Programming Language\\n\\nB... \n", + "1 # AVAP Language Specification Summary\\n\\n## Ov... \n", + "2 Based on the provided text, **I am an AI assis... \n", + "3 Based on the provided text, **I am an AI assis... \n", + "4 Error: failed to parse JSON: unexpected end of... \n", + "5 Thank you for sharing the AVAP (Automated Virt... \n", + "6 # AVAP™ Programming Guide: Core Concepts & Fun... \n", + "7 Thank you for sharing the detailed documentati... \n", + "8 Based on the provided text, **I am an AI assis... \n", + "9 ```json\\n{\\n \"title\": \"Expressions in AVAP™\",... \n", + "\n", + " context \n", + "0 [1] id=chunk-1 source=1_Introduction.txt\\nIntr... \n", + "1 [1] id=chunk-1 source=1_Introduction.txt\\nIntr... \n", + "2 [1] id=chunk-1 source=12_Loop_statement.txt\\nS... \n", + "3 [1] id=chunk-1 source=16_Function_glossary.txt... \n", + "4 \n", + "5 [1] id=chunk-1 source=13_Api_inbound_interface... \n", + "6 [1] id=chunk-1 source=16_Function_glossary.txt... \n", + "7 [1] id=chunk-1 source=9_Expressions_in_avap.tx... \n", + "8 [1] id=chunk-1 source=16_Function_glossary.txt... \n", + "9 [1] id=chunk-1 source=1_Introduction.txt\\nIntr... \n" + ] + } + ], + "source": [ + "avap_files = list((Path(DOCS_DIR) / 'samples').glob('*.avap'))\n", + "print(f\"Found {len(avap_files)} .avap files\\n\")\n", + "\n", + "rows = []\n", + "\n", + "for avap_file in sorted(avap_files):\n", + " filename = avap_file.name\n", + " \n", + " with open(avap_file, 'r', encoding='utf-8') as f:\n", + " file_content = f.read()\n", + " \n", + " prompt = f\"\"\"Please analyze and explain what this AVAP file does:\n", + "\n", + "File: {filename}\n", + "Content:\n", + "```\n", + "{file_content}\n", + "```\n", + "\n", + "Provide a clear, concise explanation of the purpose and functionality of this AVAP code.\"\"\"\n", + " \n", + " print(f\"Processing: {filename}...\")\n", + " \n", + " try:\n", + " full_response = []\n", + " for event in agentic_graph.stream(\n", + " {\"messages\": [HumanMessage(content=prompt)]},\n", + " stream_mode=\"values\",\n", + " ):\n", + " full_response.append(event[\"messages\"][-1])\n", + " \n", + " final_msg = full_response[-1]\n", + " answer = final_msg.content if hasattr(final_msg, 'content') else str(final_msg)\n", + " \n", + " context = \"\"\n", + " for msg in full_response:\n", + " if hasattr(msg, 'tool_calls') and msg.tool_calls:\n", + " pass\n", + " elif hasattr(msg, 'name') and msg.name == 'context_retrieve':\n", + " context = msg.content\n", + " \n", + " rows.append({\n", + " 'filename': filename,\n", + " 'answer': answer,\n", + " 'context': context\n", + " })\n", + " print(f\" ✓ Processed successfully\\n\")\n", + " \n", + " except Exception as e:\n", + " print(f\" ✗ Error: {str(e)}\\n\")\n", + " rows.append({\n", + " 'filename': filename,\n", + " 'answer': f\"Error: {str(e)}\",\n", + " 'context': \"\"\n", + " })\n", + "\n", + "\n", + "df_results = pd.DataFrame(rows)\n", + "print(f\"\\nCompleted! Generated {len(df_results)} results.\\n\")\n", + "print(df_results.head(10))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "79bd90fd", + "metadata": {}, + "outputs": [ + { + "ename": "", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[1;31mThe Kernel crashed while executing code in the current cell or a previous cell. \n", + "\u001b[1;31mPlease review the code in the cell(s) to identify a possible cause of the failure. \n", + "\u001b[1;31mClick here for more info. \n", + "\u001b[1;31mView Jupyter log for further details." + ] + } + ], + "source": [ + "df_results.to_parquet(DATA_DIR / 'interim' / 'rag_agent_sample_qa.parquet')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "assistance-engine", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/research/agents/n00 LangGraph Agent v1.ipynb b/research/agents/n00 LangGraph Agent v1.ipynb new file mode 100644 index 0000000..95c6142 --- /dev/null +++ b/research/agents/n00 LangGraph Agent v1.ipynb @@ -0,0 +1,318 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9f97dd1e", + "metadata": {}, + "source": [ + "# Libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9e974df6", + "metadata": {}, + "outputs": [], + "source": [ + "from __future__ import annotations\n", + "\n", + "from typing import List, Dict, Any, Optional, TypedDict\n", + "from pydantic import BaseModel, Field\n", + "import os\n", + "from elasticsearch import Elasticsearch\n", + "from langchain_community.embeddings import OllamaEmbeddings\n", + "from langchain_community.llms import Ollama\n", + "from langchain_core.messages import HumanMessage, SystemMessage\n", + "from langchain_elasticsearch import ElasticsearchStore\n", + "from langgraph.graph import StateGraph, END" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30edcecc", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_837760/4144511709.py:6: LangChainDeprecationWarning: The class `OllamaEmbeddings` was deprecated in LangChain 0.3.1 and will be removed in 1.0.0. An updated version of the class exists in the `langchain-ollama package and should be used instead. To use it run `pip install -U `langchain-ollama` and import as `from `langchain_ollama import OllamaEmbeddings``.\n", + " embeddings = OllamaEmbeddings(\n", + "/tmp/ipykernel_837760/4144511709.py:8: LangChainDeprecationWarning: The class `Ollama` was deprecated in LangChain 0.3.1 and will be removed in 1.0.0. An updated version of the class exists in the `langchain-ollama package and should be used instead. To use it run `pip install -U `langchain-ollama` and import as `from `langchain_ollama import OllamaLLM``.\n", + " llm = Ollama(base_url=base_url, model=model_name)\n" + ] + } + ], + "source": [ + "es = Elasticsearch(os.getenv(\"ELASTICSEARCH_LOCAL_URL\"))\n", + "index_name = os.getenv(\"ELASTICSEARCH_INDEX\")\n", + "base_url = os.getenv(\"LLM_BASE_LOCAL_URL\")\n", + "model_name = os.getenv(\"OLLAMA_MODEL_NAME\")\n", + "\n", + "embeddings = OllamaEmbeddings(base_url=base_url, model=model_name)\n", + "llm = Ollama(base_url=base_url, model=model_name)\n", + "\n", + "vector_store = ElasticsearchStore(\n", + " es_url=es,\n", + " index_name=index_name,\n", + " embedding=embeddings,\n", + " query_field=\"text\",\n", + " vector_query_field=\"embedding\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "1e1dd107", + "metadata": {}, + "source": [ + "# State" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "19e723e2", + "metadata": {}, + "outputs": [], + "source": [ + "class RAGState(TypedDict, total=False):\n", + " question: str\n", + " query_embedding: List[float]\n", + " docs: List[Dict[str, Any]]\n", + " answer: str\n", + " parsed: Dict[str, Any]" + ] + }, + { + "cell_type": "markdown", + "id": "90162558", + "metadata": {}, + "source": [ + "# Schema" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "acfe33d8", + "metadata": {}, + "outputs": [], + "source": [ + "class AnswerSchema(BaseModel):\n", + " answer: str = Field(..., description=\"Final answer to the user\")\n", + " citations: List[str] = Field(default_factory=list, description=\"List of sources/ids used\")\n", + " confidence: str = Field(..., description=\"low|medium|high\")" + ] + }, + { + "cell_type": "markdown", + "id": "88a4aba1", + "metadata": {}, + "source": [ + "# ES Retrieval" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "6c9e9b89", + "metadata": {}, + "outputs": [], + "source": [ + "def es_vector_search(\n", + " es_client: Elasticsearch,\n", + " index: str,\n", + " query_vector: List[float],\n", + " k: int = 5,\n", + " num_candidates: int = 50,\n", + " title_filter: Optional[str] = None,\n", + "):\n", + " \"\"\"\n", + " Uses Elasticsearch kNN search against your field: 'vector'\n", + " and returns _source: text + metadata.title (matches your mapping).\n", + " \"\"\"\n", + " body: Dict[str, Any] = {\n", + " \"knn\": {\n", + " \"field\": \"vector\", # <-- your mapping\n", + " \"query_vector\": query_vector,\n", + " \"k\": k,\n", + " \"num_candidates\": num_candidates,\n", + " },\n", + " \"_source\": [\"text\", \"metadata.title\"], # <-- your mapping\n", + " }\n", + "\n", + " # Optional: filter by title (exact via keyword subfield, or fuzzy via text)\n", + " if title_filter:\n", + " body[\"query\"] = {\n", + " \"bool\": {\n", + " \"filter\": [\n", + " {\"term\": {\"metadata.title.keyword\": title_filter}}\n", + " ]\n", + " }\n", + " }\n", + "\n", + " res = es_client.search(index=index, body=body)\n", + " return res[\"hits\"][\"hits\"]\n", + "\n", + "\n", + "def normalize_hits(hits: List[Dict[str, Any]]) -> List[Dict[str, Any]]:\n", + " docs: List[Dict[str, Any]] = []\n", + " for h in hits:\n", + " src = h.get(\"_source\", {}) or {}\n", + " meta = src.get(\"metadata\", {}) or {}\n", + " docs.append(\n", + " {\n", + " \"id\": h.get(\"_id\"),\n", + " \"score\": h.get(\"_score\"),\n", + " \"text\": src.get(\"text\", \"\"),\n", + " \"title\": meta.get(\"title\", None),\n", + " }\n", + " )\n", + " return docs\n", + "\n", + "\n", + "def format_context(docs: List[Dict[str, Any]]) -> str:\n", + " chunks = []\n", + " for i, d in enumerate(docs, 1):\n", + " title = d.get(\"title\") or \"Untitled\"\n", + " doc_id = d.get(\"id\") or f\"chunk-{i}\"\n", + " chunks.append(f\"[{i}] id={doc_id} title={title}\\n{d.get('text','')}\")\n", + " return \"\\n\\n\".join(chunks)\n" + ] + }, + { + "cell_type": "markdown", + "id": "2591e778", + "metadata": {}, + "source": [ + "# Nodes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69c41a89", + "metadata": {}, + "outputs": [], + "source": [ + "def embed_query(state: RAGState) -> RAGState:\n", + " q = state[\"question\"]\n", + " vec = embeddings.embed_query(q)\n", + " return {\"query_embedding\": vec}\n", + "\n", + "\n", + "def retrieve(state: RAGState) -> RAGState:\n", + " base_retriever = vector_store.as_retriever(\n", + " search_type=\"similarity\",\n", + " search_kwargs={\"k\": 4}\n", + " ) \n", + " docs = base_retriever.invoke(\"What does the text say about agricultural techniques?\")\n", + " docs = normalize_hits(hits)\n", + " return {\"docs\": docs}\n", + "\n", + "\n", + "def generate_answer(state: RAGState) -> RAGState:\n", + " question = state[\"question\"]\n", + " docs = state.get(\"docs\", [])\n", + " context = format_context(docs)\n", + "\n", + " system = SystemMessage(\n", + " content=(\n", + " \"You are a helpful RAG assistant. Use ONLY the provided context. \"\n", + " \"If the context is insufficient, say what is missing and ask a precise follow-up question. \"\n", + " \"Cite sources like [1], [2] based on the context chunks.\"\n", + " )\n", + " )\n", + " user = HumanMessage(\n", + " content=f\"Question:\\n{question}\\n\\nContext:\\n{context}\\n\\nWrite the best possible answer.\"\n", + " )\n", + " resp = llm.invoke([system, user])\n", + " # Handle both string and message object responses\n", + " answer = resp.content if hasattr(resp, 'content') else resp\n", + " return {\"answer\": answer}" + ] + }, + { + "cell_type": "markdown", + "id": "f3933c13", + "metadata": {}, + "source": [ + "# Graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d6810eb8", + "metadata": {}, + "outputs": [], + "source": [ + "graph = StateGraph(RAGState)\n", + "\n", + "graph.add_node(\"embed_query\", embed_query)\n", + "graph.add_node(\"retrieve\", retrieve)\n", + "graph.add_node(\"generate_answer\", generate_answer)\n", + "\n", + "graph.set_entry_point(\"embed_query\")\n", + "graph.add_edge(\"embed_query\", \"retrieve\")\n", + "graph.add_edge(\"retrieve\", \"generate_answer\")\n", + "graph.add_edge(\"generate_answer\", END)\n", + "\n", + "dag_app = graph.compile()" + ] + }, + { + "cell_type": "markdown", + "id": "6528efac", + "metadata": {}, + "source": [ + "# Retrieve" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "568708ba", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "RAW ANSWER:\n", + " New agricultural techniques include vertical farming, which integrates plants into multi-level structures in densely populated cities to optimize space and reduce transportation dependency. This approach uses hydroponic systems and automated nutrient control to grow food without traditional soil. Additionally, agriculture buildings are designed to seamlessly integrate with urban environments, addressing economic challenges while enhancing sustainability through the use of renewable energy sources.\n" + ] + } + ], + "source": [ + "out = dag_app.invoke({\"question\": \"What are new agricultural techniques about?\"})\n", + "\n", + "print(\"RAW ANSWER:\\n\", out[\"answer\"])" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "assistance-engine", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/research/agents/n00 LangGraph Agent v2.ipynb b/research/agents/n00 LangGraph Agent v2.ipynb new file mode 100644 index 0000000..bf6680f --- /dev/null +++ b/research/agents/n00 LangGraph Agent v2.ipynb @@ -0,0 +1,415 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9f97dd1e", + "metadata": {}, + "source": [ + "# Libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 145, + "id": "9e974df6", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import re\n", + "from typing import TypedDict, List, Optional, Annotated\n", + "from IPython.display import Image, display\n", + "\n", + "from langchain_core.documents import Document\n", + "from langchain_core.messages import BaseMessage, SystemMessage\n", + "from langchain_core.tools import tool\n", + "from langgraph.checkpoint.memory import InMemorySaver\n", + "from langgraph.graph.message import add_messages\n", + "from langchain_ollama import ChatOllama, OllamaEmbeddings\n", + "from langchain_elasticsearch import ElasticsearchStore\n", + "from langgraph.graph import StateGraph, END\n", + "from langgraph.prebuilt import ToolNode\n", + "from langfuse import get_client, Langfuse\n", + "from langfuse.langchain import CallbackHandler\n", + "\n", + "from typing import TypedDict, List, Optional, Annotated, Literal\n", + "from pydantic import BaseModel, Field\n", + "from langchain_core.messages import BaseMessage, SystemMessage, AIMessage" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30edcecc", + "metadata": {}, + "outputs": [], + "source": [ + "ES_URL = os.getenv(\"ELASTICSEARCH_LOCAL_URL\")\n", + "INDEX_NAME = os.getenv(\"ELASTICSEARCH_INDEX\")\n", + "CODE_INDE\n", + "BASE_URL = os.getenv(\"OLLAMA_LOCAL_URL\")\n", + "MODEL_NAME = os.getenv(\"OLLAMA_MODEL_NAME\")\n", + "EMB_MODEL_NAME = os.getenv(\"OLLAMA_EMB_MODEL_NAME\")\n", + "LANGFUSE_PUBLIC_KEY = os.getenv(\"LANGFUSE_PUBLIC_KEY\")\n", + "LANGFUSE_SECRET_KEY = os.getenv(\"LANGFUSE_SECRET_KEY\")\n", + "LANGFUSE_HOST = os.getenv(\"LANGFUSE_HOST\")\n", + "\n", + "\n", + "embeddings = OllamaEmbeddings(base_url=BASE_URL, model=EMB_MODEL_NAME)\n", + "llm = ChatOllama(base_url=BASE_URL, model=MODEL_NAME, temperature=0)\n", + "\n", + "vector_store = ElasticsearchStore(\n", + " es_url=ES_URL,\n", + " index_name=INDEX_NAME,\n", + " embedding=embeddings,\n", + " query_field=\"text\",\n", + " vector_query_field=\"vector\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "873ea2f6", + "metadata": {}, + "source": [ + "### State" + ] + }, + { + "cell_type": "code", + "execution_count": 147, + "id": "5f8c88cf", + "metadata": {}, + "outputs": [], + "source": [ + "class AgentState(TypedDict):\n", + " messages: Annotated[list, add_messages]" + ] + }, + { + "cell_type": "code", + "execution_count": 148, + "id": "30473bce", + "metadata": {}, + "outputs": [], + "source": [ + "class AgentResponse(BaseModel):\n", + " \"\"\"\n", + " Structured output contract for final assistant responses.\n", + " \"\"\"\n", + " language: Literal[\"en\"] = Field(\n", + " description=\"ISO code. Must always be 'en'.\"\n", + " )\n", + " content: str = Field(\n", + " description=\"Final answer in English.\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "1d60c120", + "metadata": {}, + "source": [ + "### Tools" + ] + }, + { + "cell_type": "code", + "execution_count": 149, + "id": "f0a21230", + "metadata": {}, + "outputs": [], + "source": [ + "retrieve_kwargs = {\"k\": 5}" + ] + }, + { + "cell_type": "code", + "execution_count": 150, + "id": "f9359747", + "metadata": {}, + "outputs": [], + "source": [ + "def format_context(docs: List[Document]) -> str:\n", + " chunks: List[str] = []\n", + " for i, doc in enumerate(docs, 1):\n", + " source = (doc.metadata or {}).get(\"source\", \"Untitled\")\n", + " source_id = (doc.metadata or {}).get(\"id\", f\"chunk-{i}\")\n", + " text = doc.page_content or \"\"\n", + " chunks.append(f\"[{i}] id={source_id} source={source}\\n{text}\")\n", + " return \"\\n\\n\".join(chunks)\n", + "\n", + "\n", + "@tool\n", + "def retrieve(query: str) -> str:\n", + " \"\"\"This tool retrieves relevant documents from the vector store based on the input query and formats them for the agent's response.\n", + " Args:\n", + " query (str): The input query for which to retrieve relevant documents.\n", + " \"\"\"\n", + " retriever = vector_store.as_retriever(\n", + " search_type=\"similarity\",\n", + " search_kwargs=retrieve_kwargs,\n", + " )\n", + " docs = retriever.invoke(query)\n", + " return format_context(docs)" + ] + }, + { + "cell_type": "code", + "execution_count": 151, + "id": "e5247ab9", + "metadata": {}, + "outputs": [], + "source": [ + "def should_continue(state: AgentState) -> str:\n", + " last = state[\"messages\"][-1]\n", + " \n", + " if getattr(last, \"tool_calls\", None):\n", + " return \"tools\"\n", + " return \"end\"" + ] + }, + { + "cell_type": "code", + "execution_count": 152, + "id": "a644f6fa", + "metadata": {}, + "outputs": [], + "source": [ + "tools = [retrieve]\n", + "tool_node = ToolNode(tools)\n", + "memory = InMemorySaver()" + ] + }, + { + "cell_type": "markdown", + "id": "395966e2", + "metadata": {}, + "source": [ + "### Agent" + ] + }, + { + "cell_type": "code", + "execution_count": 153, + "id": "36d0f54e", + "metadata": {}, + "outputs": [], + "source": [ + "def _message_text(message: BaseMessage) -> str:\n", + " content = getattr(message, \"content\", \"\")\n", + " if isinstance(content, str):\n", + " return content\n", + " if isinstance(content, list):\n", + " return \" \".join(str(item) for item in content)\n", + " return str(content)\n", + "\n", + "\n", + "def _last_user_query(messages: List[BaseMessage]) -> str:\n", + " for message in reversed(messages):\n", + " message_type = getattr(message, \"type\", \"\")\n", + " if message_type == \"human\":\n", + " return _message_text(message)\n", + " return _message_text(messages[-1]) if messages else \"\"\n", + "\n", + "\n", + "def _last_assistant_text(messages: List[BaseMessage]) -> str:\n", + " for message in reversed(messages):\n", + " message_type = getattr(message, \"type\", \"\")\n", + " if message_type == \"ai\":\n", + " return _message_text(message)\n", + " return \"\"\n", + "\n", + "\n", + "MAX_LANGUAGE_RETRIES = 5\n", + "\n", + "\n", + "def agent(state: AgentState) -> AgentState:\n", + " messages: List[BaseMessage] = state[\"messages\"]\n", + " user_query = _last_user_query(messages)\n", + "\n", + " retrieved_context = retrieve.invoke({\"query\": user_query})\n", + "\n", + " system = SystemMessage(\n", + " content=(\n", + " \"\"\"\n", + " You are an AVAP language assistant focused on accurate and grounded answers.\n", + "\n", + " Rules:\n", + " 1. ALWAYS use the provided retrieval context from Elasticsearch FIRST.\n", + " 2. Use ONLY the retrieved context to produce the final answer. Do NOT invent AVAP commands.\n", + " 3. ALWAYS reply in English only. Never respond in any other language.\n", + " 4. If asked for code/snippet, include exactly ONE fenced AVAP block (```avap ... ```).\n", + " 5. Never output internal reasoning or unrelated text.\n", + "\n", + " Retrieved context:\n", + " \"\"\"\n", + " + retrieved_context\n", + " )\n", + " )\n", + "\n", + "\n", + "\n", + " structured_model = llm.with_structured_output(AgentResponse)\n", + " structured_response: AgentResponse = structured_model.invoke(\n", + " [system, *messages]\n", + " )\n", + "\n", + " # Keep LangGraph message flow unchanged\n", + " final_message = AIMessage(content=structured_response.content)\n", + " return {\"messages\": [*messages, final_message]}\n", + "\n", + " # model = llm.bind_tools(tools)\n", + " # response = model.invoke([system, *messages])\n", + "\n", + " # return {\"messages\": [*messages, response]}" + ] + }, + { + "cell_type": "markdown", + "id": "ef55bca3", + "metadata": {}, + "source": [ + "### Graph" + ] + }, + { + "cell_type": "code", + "execution_count": 154, + "id": "fae46a58", + "metadata": {}, + "outputs": [], + "source": [ + "graph = StateGraph(AgentState)\n", + "graph.add_node(\"agent\", agent)\n", + "\n", + "\n", + "graph.set_entry_point(\"agent\")\n", + "graph.add_edge(\"agent\", END)\n", + "\n", + "# Alternative mode (single pass) - kept commented for quick rollback.\n", + "# graph.set_entry_point(\"agent\")\n", + "# graph.add_edge(\"agent\", END)\n", + "\n", + "agent_graph = graph.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 155, + "id": "2fec3fdb", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "try:\n", + " display(Image(agent_graph.get_graph().draw_mermaid_png()))\n", + "except Exception:\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "1e9aff05", + "metadata": {}, + "source": [ + "### Test" + ] + }, + { + "cell_type": "code", + "execution_count": 156, + "id": "8569cf39", + "metadata": {}, + "outputs": [], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"5\"}, \n", + " #\"callbacks\": [langfuse_handler],\n", + " #\"run_name\": \"rag-local-test\"differences between getDatetime() and getTimeStamp() functions in AVAP\n", + " }\n", + "\n", + "def stream_graph_updates(user_input: str):\n", + " for event in agent_graph.stream(\n", + " {\"messages\": [{\"role\": \"user\", \"content\": user_input}]},\n", + " #config=config,\n", + " stream_mode=\"values\",\n", + " ):\n", + " event[\"messages\"][-1].pretty_print()" + ] + }, + { + "cell_type": "code", + "execution_count": 162, + "id": "a1a1f3cf", + "metadata": {}, + "outputs": [], + "source": [ + "user_input = (\n", + " \"Generate two variables, asigning them one number to each, do it in AVAP language.\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 163, + "id": "53b89690", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "Generate two variables, asigning them one number to each, do it in AVAP language.\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "user: generate two variables, assigning them one number to each, you assign themize in, avap language.\n" + ] + } + ], + "source": [ + "a = stream_graph_updates(user_input)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "136f420c", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "assistance-engine", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/research/agents/n00 LangGraph agent simple.ipynb b/research/agents/n00 LangGraph agent simple.ipynb new file mode 100644 index 0000000..d83bf6e --- /dev/null +++ b/research/agents/n00 LangGraph agent simple.ipynb @@ -0,0 +1,472 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9f97dd1e", + "metadata": {}, + "source": [ + "# Libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "9e974df6", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import sys\n", + "from pathlib import Path\n", + "from typing import TypedDict, List, Optional, Annotated, Literal\n", + "from IPython.display import Image, display\n", + "from pydantic import BaseModel, Field\n", + "\n", + "# Ensure the project root is on the path so `src` is importable\n", + "_project_root = str(Path(__file__).resolve().parents[2]) if \"__file__\" in dir() else str(Path.cwd().parents[1])\n", + "if _project_root not in sys.path:\n", + " sys.path.insert(0, _project_root)\n", + "\n", + "from langchain_core.documents import Document\n", + "from langchain_core.messages import BaseMessage, SystemMessage, AIMessage\n", + "from langchain_core.tools import tool\n", + "from langgraph.checkpoint.memory import InMemorySaver\n", + "from langgraph.graph.message import add_messages\n", + "from langchain_ollama import ChatOllama, OllamaEmbeddings\n", + "from langchain_elasticsearch import ElasticsearchStore\n", + "from langgraph.graph import StateGraph, END\n", + "from langgraph.prebuilt import ToolNode, tools_condition\n", + "from langfuse import Langfuse\n", + "from langfuse.decorators import observe\n", + "\n", + "from src.utils.llm_factory import create_chat_model\n", + "from src.utils.emb_factory import create_embedding_model\n", + "from src.config import (\n", + " ELASTICSEARCH_LOCAL_URL,\n", + " ELASTICSEARCH_INDEX,\n", + " OLLAMA_MODEL_NAME,\n", + " OLLAMA_EMB_MODEL_NAME,\n", + ")\n", + "\n", + "# Initialize Langfuse client for tracing\n", + "langfuse = Langfuse()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "30edcecc", + "metadata": {}, + "outputs": [], + "source": [ + "llm = create_chat_model(\n", + " provider=\"ollama\",\n", + " model=OLLAMA_MODEL_NAME,\n", + " temperature=0,\n", + " validate_model_on_init=True,\n", + ")\n", + "embeddings = create_embedding_model(\n", + " provider=\"ollama\",\n", + " model=OLLAMA_EMB_MODEL_NAME,\n", + ")\n", + "vector_store = ElasticsearchStore(\n", + " es_url=ELASTICSEARCH_LOCAL_URL,\n", + " index_name=ELASTICSEARCH_INDEX,\n", + " embedding=embeddings,\n", + " query_field=\"text\",\n", + " vector_query_field=\"vector\",\n", + " # strategy=ElasticsearchStore.ApproxRetrievalStrategy(\n", + " # hybrid=True,\n", + " # rrf={\"rank_constant\": 60, \"window_size\": 100}\n", + " # )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "873ea2f6", + "metadata": {}, + "source": [ + "### State" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "5f8c88cf", + "metadata": {}, + "outputs": [], + "source": [ + "class AgentState(TypedDict):\n", + " messages: Annotated[list, add_messages]\n", + " reformulated_query: str\n", + " context: str" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "fd8ed542", + "metadata": {}, + "outputs": [], + "source": [ + "class AgenticAgentState(TypedDict):\n", + " messages: Annotated[list, add_messages]" + ] + }, + { + "cell_type": "markdown", + "id": "1d60c120", + "metadata": {}, + "source": [ + "### Tools" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "f0a21230", + "metadata": {}, + "outputs": [], + "source": [ + "retrieve_kwargs = {\"k\": 3}" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "f9359747", + "metadata": {}, + "outputs": [], + "source": [ + "def format_context(docs: List[Document]) -> str:\n", + " chunks: List[str] = []\n", + " for i, doc in enumerate(docs, 1):\n", + " source = (doc.metadata or {}).get(\"source\", \"Untitled\")\n", + " source_id = (doc.metadata or {}).get(\"id\", f\"chunk-{i}\")\n", + " text = doc.page_content or \"\"\n", + " chunks.append(f\"[{i}] id={source_id} source={source}\\n{text}\")\n", + " return \"\\n\\n\".join(chunks)\n", + "\n", + "\n", + "@tool\n", + "@observe(name=\"context_retrieve\")\n", + "def context_retrieve(query: str) -> str:\n", + " \"\"\"Consults vector store to respond AVAP related questions\n", + " Args:\n", + " query (str): The input query for which to retrieve relevant documents.\n", + " \"\"\"\n", + " retriever = vector_store.as_retriever(\n", + " search_type=\"similarity\",\n", + " search_kwargs=retrieve_kwargs,\n", + " )\n", + " docs = retriever.invoke(query)\n", + " return format_context(docs)" + ] + }, + { + "cell_type": "markdown", + "id": "395966e2", + "metadata": {}, + "source": [ + "### Agent" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "66ae23f0", + "metadata": {}, + "outputs": [], + "source": [ + "REFORMULATE_PROMPT = SystemMessage(\n", + " content=(\n", + " \"You are a deterministic query rewriting function.\\n\"\n", + " \"You convert natural language questions into keyword search queries.\\n\\n\"\n", + " \"Strict constraints:\\n\"\n", + " \"1. Keep function names and technical tokens unchanged.\\n\"\n", + " \"2. Remove filler phrases.\\n\"\n", + " \"3. Do not answer.\\n\"\n", + " \"4. Do not explain.\\n\"\n", + " \"5. Do not generate code.\\n\"\n", + " \"6. Return a single-line query only.\\n\"\n", + " \"7. If already optimal, return unchanged.\\n\"\n", + " )\n", + ")\n", + "\n", + "GENERATE_PROMPT = SystemMessage(\n", + " content=\"\"\"You are an agent designed to assist users with AVAP (Advanced Virtual API Programming) language.\n", + " It's a new language, so you should know nothing about it.\n", + " Use ONLY the provided context to answer AVAP-related questions.\n", + " If the context does not contain enough information, say so honestly.\n", + " If the question is not related to AVAP, answer based on your general knowledge.\n", + "\n", + " Context:\n", + " {context}\"\"\"\n", + ")\n", + "\n", + "AGENTIC_PROMPT = SystemMessage(\n", + " content=\"\"\"You are an agent designed to assist users with AVAP (Advanced Virtual API Programming) language.\n", + " It's a new language, so you should know nothing about it.\n", + " Use ONLY the provided 'context_retrieve' tool to answer AVAP-related questions.\n", + " The 'context_retrieve' tool receives a user query (as a string) and returns relevant context from a vector store.\n", + " If the context does not contain enough information, say so honestly.\n", + " If the question is not related to AVAP, answer based on your general knowledge.\n", + " \"\"\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "36d0f54e", + "metadata": {}, + "outputs": [], + "source": [ + "@observe(name=\"reformulate_query\")\n", + "def reformulate(state: AgentState) -> AgentState:\n", + " \"\"\"Use the LLM to rewrite the user query for better retrieval.\"\"\"\n", + " user_msg = state[\"messages\"][-1]\n", + " resp = llm.invoke([REFORMULATE_PROMPT, user_msg])\n", + " reformulated = resp.content.strip()\n", + " print(f\"[reformulate] '{user_msg.content}' → '{reformulated}'\")\n", + " return {\"reformulated_query\": reformulated}\n", + "\n", + "\n", + "@observe(name=\"retrieve_documents\")\n", + "def retrieve(state: AgentState) -> AgentState:\n", + " \"\"\"Retrieve context using the reformulated query.\"\"\"\n", + " query = state[\"reformulated_query\"]\n", + " docs = vector_store.as_retriever(\n", + " search_type=\"similarity\",\n", + " search_kwargs=retrieve_kwargs,\n", + " ).invoke(query)\n", + " context = format_context(docs)\n", + " print(f\"[retrieve] {len(docs)} docs fetched\")\n", + " print(context)\n", + " return {\"context\": context}\n", + "\n", + "\n", + "@observe(name=\"generate_response\")\n", + "def generate(state: AgentState) -> AgentState:\n", + " \"\"\"Generate the final answer using retrieved context.\"\"\"\n", + " prompt = SystemMessage(\n", + " content=GENERATE_PROMPT.content.format(context=state[\"context\"])\n", + " )\n", + " resp = llm.invoke([prompt] + state[\"messages\"])\n", + " return {\"messages\": [resp]}\n" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "f073edc9", + "metadata": {}, + "outputs": [], + "source": [ + "@observe(name=\"agent_node\")\n", + "def agent(state: AgentState) -> AgentState:\n", + " llm_with_tools = llm.bind_tools(tools)\n", + " return {\"messages\": [llm_with_tools.invoke([SystemMessage(content=AGENTIC_PROMPT.content)] + state[\"messages\"])]}\n" + ] + }, + { + "cell_type": "markdown", + "id": "ef55bca3", + "metadata": {}, + "source": [ + "### Graph" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "f7a0993f", + "metadata": {}, + "outputs": [], + "source": [ + "tools = [context_retrieve]\n", + "tool_node = ToolNode(tools=tools)\n", + "memory = InMemorySaver()\n", + "\n", + "graph_builder = StateGraph(AgenticAgentState)\n", + "\n", + "graph_builder.add_node(\"agent\", agent)\n", + "graph_builder.add_node(\"tools\", tool_node)\n", + "\n", + "graph_builder.set_entry_point(\"agent\")\n", + "graph_builder.add_conditional_edges(\n", + " \"agent\",\n", + " tools_condition,\n", + ")\n", + "graph_builder.add_edge(\"tools\", \"agent\")\n", + "\n", + "agentic_graph = graph_builder.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "2fec3fdb", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "try:\n", + " display(Image(agentic_graph.get_graph().draw_mermaid_png()))\n", + "except Exception:\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "1e9aff05", + "metadata": {}, + "source": [ + "### Test" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8569cf39", + "metadata": {}, + "outputs": [], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"5\"}, \n", + " #\"callbacks\": [langfuse_handler],\n", + " #\"run_name\": \"rag-local-test\"differences between getDatetime() and getTimeStamp() functions in AVAP\n", + " }\n", + "\n", + "@observe(name=\"stream_graph_updates\")\n", + "def stream_graph_updates(user_input: str, graph: StateGraph):\n", + " langfuse\n", + " for event in graph.stream(\n", + " {\"messages\": [{\"role\": \"user\", \"content\": user_input}]},\n", + " #config=config,\n", + " stream_mode=\"values\",\n", + " ):\n", + " event[\"messages\"][-1].pretty_print()\n", + " # last_msg = event[\"messages\"][-1]\n", + " # if isinstance(last_msg, AIMessage):\n", + " # last_msg.pretty_print()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "a1a1f3cf", + "metadata": {}, + "outputs": [], + "source": [ + "user_input = \"\"\"Suppose you want to create a table called users in a database called myDatabase, with two columns: username of type VARCHAR and age of type INTEGER. How would you do that in AVAP?\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "53b89690", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "Suppose you want to create a table called users in a database called myDatabase, with two columns: username of type VARCHAR and age of type INTEGER. How would you do that in AVAP?\n", + "[reformulate] 'Suppose you want to create a table called users in a database called myDatabase, with two columns: username of type VARCHAR and age of type INTEGER. How would you do that in AVAP?' → 'create table users (username varchar, age integer)'\n", + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "Suppose you want to create a table called users in a database called myDatabase, with two columns: username of type VARCHAR and age of type INTEGER. How would you do that in AVAP?\n", + "[retrieve] 3 docs fetched\n", + "[1] id=chunk-1 source=16_Function_glossary.txt\n", + "Function Glossary randomString() The randomString() command generates a random string based on a specified pattern and stores it in a target variable. It is especially useful when random strings are needed to conform to a specific format, such as passwords or identifiers. Parameters Pattern Type: var Description: A regular expression (regex) pattern that defines the characters and structure of the string to be generated. It can be a direct value or a variable containing the pattern. For example, [a-zA-Z0-9] will generate a string that includes uppercase letters, lowercase letters, and numbers. Length Type: var Description: An integer value specifying the length of the random string to be generated. It can be a direct value or a variable containing the desired length. This value determines how many characters the resulting string will have. TargetVariable Type: var Description: The variable where the generated string will be stored. This variable should be used later in the program. Unlike the other parameters, this must be a variable and not a direct value. Usage Example // Direct call with values: randomString('[a-zA-Z0-9]', 8, generatedPassword) // Call using variables: pattern = '[a-zA-Z0-9]' length = 8 randomString(pattern, length, generatedPassword) stampToDatetime() The stampToDatetime() command converts a timestamp value to a date and time according to a specified format, applying a possible time difference, and stores the result in a target variable. It is useful for manipulating and formatting time values into different representations. Parameters timestamp Type: var Description: A value representing a timestamp, which can be provided directly or through a variable. This value is the starting point for conversion to a date and time format. Format Type: var Description: A format string that defines how the resulting date and time should be presented. This string follows the same conventions used in Python for formatting dates and times. Common symbols include: %Y: Year with four digits (e.g., 2024) %m: Month with two digits (01 to 12) %d: Day of the month with two digits (01 to 31) %H: Hour in 24-hour format (00 to 23) %M: Minutes (00 to 59) %S: Seconds (00 to 59) For example, the format %Y-%m-%d %H:%M:%S converts a timestamp into a string like 2024-08-25 14:30:00. It can be a direct value or a variable containing the desired format. TimeDelta Type: var Description: An optional value representing a time adjustment (positive or negative) applied to the timestamp before conversion. This value can be provided directly or through a variable and is expressed in seconds. TargetVariable Type: var Description: The variable where the resulting date and time from the conversion will be stored. Unlike the other parameters, this must be a variable and not a direct value. Usage Example // Direct call with values: stampToDatetime(1692966600, '%Y-%m-%d %H:%M:%S', 3600, convertedDatetime) // Call using variables: timestamp = 1692966600 format = '%Y-%m-%d %H:%M:%S' adjustment = 3600 stampToDatetime(timestamp, format, adjustment, convertedDatetime) In the first example, a timestamp is converted to a date and time in the format \"%Y-%m-%d %H:%M:%S\", applying a 3600-second (1-hour) adjustment, and the result is stored in the variable convertedDatetime. In the second example, variables are used to define the timestamp, format, and adjustment. getTimeStamp() The getTimeStamp() command converts a date and time string, given in a specific format, to a timestamp value. Additionally, it allows for an optional time adjustment before storing the result in a target variable. This command is useful for converting human-readable date and time representations to a numeric timestamp format, which can be used in calculations or time comparisons. Parameters DateString Type: var Description: A string representing a date and time. This string must follow the format specified in the Format parameter. It can be a direct value or a variable containing the date string. Format Type: var Description: A format string that defines how to interpret the date and time string (DateString). This string follows Python's conventions for formatting and parsing dates and times. Some common symbols include: %Y: Year with four digits (e.g., 2024) %m: Month with two digits (01 to 12) %d: Day of the month with two digits (01 to 31) %H: Hour in 24-hour format (00 to 23) %M: Minutes (00 to 59) %S: Seconds (00 to 59) For example, to interpret the string \"2024-08-25 14:30:00\", the format %Y-%m-%d %H:%M:%S would be used. It can be a direct value or a variable containing the format. TimeDelta Type: var Description: An optional value representing a time adjustment (positive or negative) applied to the timestamp after conversion. This value can be provided directly or through a variable and is expressed in seconds. TargetVariable Type: var Description: The variable where the resulting timestamp from the conversion will be stored. Unlike the other parameters, this must be a variable and not a direct value. Usage Example // Direct call with values: getTimeStamp('2024-08-25 14:30:00', '%Y-%m-%d %H:%M:%S', 3600, generatedTimestamp) // Call using variables: date = '2024-08-25 14:30:00' format = '%Y-%m-%d %H:%M:%S' adjustment = 3600 getTimeStamp(date, format, adjustment, generatedTimestamp) In the first example, the date and time string \"2024-08-25 14:30:00\" is converted to a timestamp, applying a 3600-second (1-hour) adjustment, and the result is stored in the variable generatedTimestamp. In the second example, variables are used to define the date, format, and adjustment. getRegex() The getRegex() command searches for matches in a source string using a regular expression (regex) pattern and stores the result in a target variable. This command is useful for extracting specific parts of a string that match a defined pattern, such as email addresses, phone numbers, or any other structure defined by a regex. Parameters SourceVariable Type: variable Description: The variable containing the source string in which to search for regex pattern matches. This string is the text on which the regex search will be applied. rePattern Type: variable Description: The variable containing the regular expression (regex) pattern that defines what to search for in the source string. This pattern should follow standard regex rules, allowing the specification of sequences of characters to identify in the source string. TargetVariable Type: variable Description: The variable where the search result will be stored. Depending on the context and the pattern used, the result could be the first match found, all matches, or even specific groups within the match. Usage Example // Direct call with values: sourceText = \"Email: user@example.com and phone: 123-456-7890\" pattern = r\"\\b\\d{3}-\\d{3}-\\d{4}\\b\" getRegex(sourceText, pattern, phoneNumber) // Call using variables: sourceText = \"Visit our website at https://www.example.com for more information.\" regexPattern = r\"https?://\\S+\" getRegex(sourceText, regexPattern, foundURL) In the first example, a phone number in the format 123-456-7890 is searched in the sourceText string and the result is stored in the phoneNumber variable. In the second example, a URL is extracted from the sourceText string using a regex that identifies URL patterns, and the result is stored in the foundURL variable. getDateTime() The getDateTime() command retrieves the current date and time, formats it according to a specified format, applies an optional time adjustment, and converts it to a specific time zone before storing the result in a target variable. It is useful for obtaining and manipulating the current date and time in different formats and time zones. Parameters Format Type: var Description: A format string that defines how the resulting date and time should be presented. This string follows the date and time formatting conventions used in Python. Some of the most common symbols include: %Y: Year with four digits (e.g., 2024) %m: Month with two digits (01 to 12) %d: Day of the month with two digits (01 to 31) %H: Hour in 24-hour format (00 to 23) %M: Minutes (00 to 59) %S: Seconds (00 to 59) For example, the format \"%Y-%m-%d %H:%M:%S\" will present the date and time as 2024-08-25 14:30:00. It can be a direct value or a variable containing the desired format. TimeDelta Type: var Description: An optional value representing a time adjustment (positive or negative) applied to the current date and time before conversion. This value can be provided directly or through a variable and is expressed in seconds. TimeZone Type: var Description: The time zone to which the date and time should be converted. This value can be a time zone identifier provided directly or through a variable. Some common time zones include: \"UTC\": Coordinated Universal Time \"America/New_York\": U.S. Eastern Time (EST/EDT) \"America/Los_Angeles\": U.S. Pacific Time (PST/PDT) \"Europe/London\": London Time (GMT/BST) \"Europe/Madrid\": Madrid Time (CET/CEST) \"Asia/Tokyo\": Tokyo Time (JST) \"Australia/Sydney\": Sydney Time (AEST/AEDT) You can use any time zone recognized by the pytz library in Python, which includes most time zones worldwide. TargetVariable Type: var Description: The variable in which the resulting date and time from the operation will be stored. Unlike the other parameters, this must be a variable and not a direct value. Usage Example // Direct call with values: getDateTime('%Y-%m-%d %H:%M:%S', 3600, 'UTC', currentTime) // Call using variables: format = '%Y-%m-%d %H:%M:%S' adjustment = 3600 timeZone = 'America/New_York' getDateTime(format, adjustment, timeZone, currentDateTime) In the first example, the current date and time are retrieved, adjusted by 3600 seconds (1 hour), converted to UTC, and stored in the variable currentTime. In the second example, variables are used to define the format, time adjustment, and time zone, with the result stored in the currentDateTime variable. encodeMD5() The encodeMD5() command generates an MD5 hash of the provided string and stores the result in a target variable. MD5 is a cryptographic hash function that produces a 128-bit value (32 hexadecimal characters), commonly used to verify data integrity. Parameters SourceVariable Type: var Description: The variable containing the text string to be encoded in MD5. It can be a direct value or a variable storing the input string. TargetVariable Type: var Description: The variable in which the resulting MD5 hash will be stored. Unlike the SourceVariable parameter, this must be a variable and not a direct value. Usage Example // Direct call with values: encodeMD5('example_string', md5Hash) // Call using variables: text = 'example_string' hashVariable = 'md5Hash' encodeMD5(text, hashVariable) In the first example, an MD5 hash is generated from the string 'example_string' and stored in the md5Hash variable. In the second example, a variable text is used to define the input string and another variable hashVariable is used to store the resulting MD5 hash. encodeSHA256() The encodeSHA256() command generates a SHA-256 hash of the provided string and stores the result in a target variable. SHA-256 is a cryptographic hash function that produces a 256-bit value (64 hexadecimal characters), offering greater security compared to MD5. Parameters SourceVariable Type: var Description: The variable containing the text string to be encoded in SHA-256. It can be a direct value or a variable storing the input string. TargetVariable Type: var Description: The variable in which the resulting SHA-256 hash will be stored. Unlike the SourceVariable parameter, this must be a variable and not a direct value. Usage Example // Direct call with values: encodeSHA256('example_string', sha256Hash) // Call using variables: text = 'example_string' hashVariable = 'sha256Hash' encodeSHA256(text, hashVariable) In the first example, a SHA-256 hash is generated from the string 'example_string' and stored in the sha256Hash variable. In the second example, a variable text is used to define the input string, and another variable hashVariable is used to store the resulting SHA-256 hash. getQueryParamList() The getQueryParamList() command extracts the query parameters from the current HTTP request and stores a list of these parameters in a target variable. This is useful for handling and processing query parameters in web applications. Parameters TargetVariable Type: var Description: The variable in which the extracted query parameter list will be stored. This should be a variable where the command's result will be saved. Command Flow Parameter Extraction: Accesses the query parameters from the current HTTP request. List Construction: Creates a list containing dictionaries, where each dictionary represents a query parameter and its associated value. Result Storage: Saves the list of parameters in the variable specified by TargetVariable. Usage Example Suppose the HTTP query has the following parameters: ?user=alice&age=30. // Define the variable to store the result queryParamsList = [] // Call the command to extract query parameters getQueryParamList(queryParamsList) // Return the list of query parameters via addResult addResult(queryParamsList) Given the query string ?user=alice&age=30, the getQueryParamList() command will generate the following list of parameters: [ {\"user\": \"alice\"}, {\"age\": \"30\"} ] getListLen() The getListLen() command calculates the length of a list and stores the result in a target variable. This command is useful for determining the number of elements in a list. Parameters SourceVariable Type: var Description: The variable containing the list whose length you want to calculate. It can be a variable that stores the list or a direct value representing the list. TargetVariable Type: var Description: The variable where the result of the list length will be stored. This should be a variable that will receive the integer value representing the number of elements in the list. Command Flow Retrieve the List: Access the list stored in the SourceVariable. Calculate the Length: Calculate the number of elements in the list. Store the Result: Save the calculated length in the variable specified by TargetVariable. Usage Example Suppose the list in myList is ['apple', 'banana', 'cherry']. // Variable definitions myList = ['apple', 'banana', 'cherry'] listLength = 0 // Call the command to calculate the length of the list getListLen(myList, listLength) // Return the list length through addResult addResult(listLength) Since the list myList has 3 elements, the getListLen() command will calculate that the length is 3. This value will be stored in the listLength variable and returned through addResult(listLength), resulting in the following output: 3 itemFromList() The itemFromList() command extracts a specific element from a list based on a given index and stores the result in a target variable. This is useful for accessing individual elements within a list. Parameters SourceVariable Type: var Description: The variable containing the list from which an element is to be extracted. It can be a variable that stores the list or a direct value representing the list. index Type: value Description: The index of the element to be extracted from the list. It must be an integer value that indicates the position of the element within the list. TargetVariable Type: var Description: The variable where the extracted element will be stored. It must be a variable that will receive the value of the element at the specified index position. Command Flow Access the List: Access the list stored in the SourceVariable. Extract the Element: Retrieve the element at the position specified by the index. Store the Result: Save the extracted element in the variable specified by TargetVariable. Usage Example Suppose the list in myList is ['apple', 'banana', 'cherry'] and you want to extract the element at index 1. // Variable definitions myList = ['apple', 'banana', 'cherry'] element = '' // Call the command to extract the element at index 1 itemFromList(myList, 1, element) // Return the extracted element through addResult addResult(element) Since index 1 corresponds to the element 'banana' in the myList, the itemFromList() command will extract 'banana' and store it in the variable element. The element variable will be returned through addResult(element), resulting in the following output: \"banana\" variableFromJSON() The variableFromJSON() command extracts the value associated with a specific key from a JSON object and stores the result in a target variable. This command is useful for accessing values within a JSON object. Parameters SourceVariable Type: var Description: The variable containing the JSON object from which a value is to be extracted. It can be a variable that stores the JSON object or a direct value representing the JSON object. key Type: value Description: The key whose value is to be extracted from the JSON object. It must be a value that represents the key within the JSON object. TargetVariable Type: var Description: The variable where the extracted value will be stored. It must be a variable that will receive the value associated with the specified key in the JSON object. Command Flow Access the JSON Object: Access the JSON object stored in the SourceVariable. Extract the Value: Retrieve the value associated with the key within the JSON object. Store the Result: Save the extracted value in the variable specified by TargetVariable. Usage Example Suppose the JSON object in jsonData is \"name\": \"Alice\", \"age\": 30 and you want to extract the value associated with the key \"name\". // Variable definitions jsonData = {\"name\": \"Alice\", \"age\": 30} nameValue = '' // Call the command to extract the value associated with the key \"name\" variableFromJSON(jsonData, \"name\", nameValue) // Return the extracted value through addResult addResult(nameValue) Since the value associated with the key \"name\" in the JSON object jsonData is \"Alice\", the variableFromJSON() command will extract \"Alice\" and store it in the variable nameValue. The nameValue variable will be returned through addResult(nameValue), resulting in the following output: \"Alice\" AddVariableToJSON() The AddVariableToJSON() command adds a new key and its corresponding value to a JSON object and stores the result in a target variable. This command is useful for updating a JSON object with new key-value pairs. Parameters Key Type: variable Description: The key to be added to the JSON object. It must be a variable that stores the key to be added. Value Type: variable Description: The value associated with the key to be added to the JSON object. It must be a variable that stores the corresponding value. TargetVariable Type: variable Description: The variable where the updated JSON object will be stored. It must be a variable that will receive the JSON object with the new key and its added value. Command Flow Access the JSON Object: Access the JSON object stored in the TargetVariable. Add the Key and Value: Add the new key and its associated value to the JSON object. Store the Result: Save the updated JSON object in the variable specified by TargetVariable. Usage Example Suppose the initial JSON object in jsonData is \"name\": \"Alice\", \"age\": 30, and you want to add a new key \"email\" with the value \"alice@example.com\". // Variable definitions jsonData = {\"name\": \"Alice\", \"age\": 30} newKey = \"email\" newValue = \"alice@example.com\" // Call the command to add the new key and value to the JSON object AddVariableToJSON(newKey, newValue, jsonData) // Return the updated JSON object through addResult addResult(jsonData) This updated JSON object will be stored in the variable jsonData and will be returned through addResult(jsonData), resulting in the following output: { \"name\": \"Alice\", \"age\": 30, \"email\": \"alice@example.com\" } variableToList() The variableToList() command converts an element into a list that contains only that element and stores the resulting list in a target variable. This command is useful to ensure that a single value is handled as a list in subsequent processing. Parameters element Type: variable Description: The variable that contains the element to be converted into a list. It can be any type of value that you want to include as the only item in the list. TargetVariable Type: variable Description: The variable in which the resulting list will be stored. It must be a variable that will receive the list with the included element. Command Flow Access the Element: Access the element stored in the element variable. Create the List: Create a list that contains only the provided element. Store the Result: Save the resulting list in the variable specified by TargetVariable. Usage Example Suppose the element in myElement is \"apple\" and you want to convert it into a list. // Variable definitions myElement = \"apple\" myList = [] // Call the command to convert the element into a list variableToList(myElement, myList) // Return the resulting list through addResult addResult(myList) Since myElement is \"apple\", the variableToList() command will convert this element into a list with a single item: [\"apple\"]. This list will be stored in the variable myList, and myList will be returned through addResult(myList), resulting in the following output: [\"apple\"] addParam() The addParam() command retrieves the value associated with a specific key from the query string of the current request and assigns this value to a target variable. This command is useful for extracting values from query parameters in an HTTP request and storing them in variables for processing. Parameters param Type: value Description: The key of the query string whose value you want to retrieve. It should be a value that represents the key in the query string. variable Type: var Description: The variable in which the retrieved value from the query string will be stored. It must be a variable that will receive the value associated with the specified key. Command Flow Retrieve the Value: Access the value associated with the param key from the query string of the current request. Assign the Value: Assign the retrieved value to the variable specified by variable. Usage Example Suppose the query string of the current request is ?user=alice&age=30, and you want to retrieve the value associated with the key \"user\". // Variable definitions userName = '' // Call the command to retrieve the value for the \"user\" key and assign it to the variable addParam(\"user\", userName) // Return the retrieved value through addResult addResult(userName) Given the query string ?user=alice&age=30, the addParam() command will retrieve the value \"alice\" associated with the key \"user\" and store it in the userName variable. The userName variable will be returned through addResult(userName), resulting in the following output: \"alice\" addResult() The addResult() command is used to return the content of a variable as part of the command or function response. It is the way to present results or processed data from commands and operations performed in the language. Parameters variable Type: var Description: The variable whose content is to be returned as the result. It should be a variable that contains the value or data you want to include in the response. Command Flow Access the Content: Access the content of the variable provided as a parameter. Return the Result: Include the content of the variable in the final response. Example Usage Suppose we have performed an operation and want to return the result stored in the result variable. // Define the variable with the result of an operation result = \"Operation completed successfully.\" // Call the command to return the content of the variable addResult(result) In this example, the addResult(result) command will return the content of the result variable, which is \"Operation completed successfully.\". This content will be presented as part of the response. Note The addResult() command is the primary mechanism for returning information and results in the language. Make sure that the variable passed to the command contains the desired data or result before calling addResult(). RequestPost() The RequestPost() command performs an HTTP POST request to a specified URL, sending a query string, headers, and a request body, and stores the result of the request in a destination variable. This command is useful for sending data to a server and handling the responses from the request. Parameters url Type: variable Description: The URL to which the POST request will be sent. It should be a variable containing the address of the resource to which the request is to be made. querystring Type: variable Description: The query string that will be appended to the URL. It should be a variable containing the query parameters in string format. headers Type: variable Description: The HTTP headers that will be included in the POST request. It should be a variable containing a dictionary of headers and their values. body Type: variable Description: The body of the POST request that will be sent to the server. It should be a variable containing the data to be sent in the request. o_result Type: variable Description: The variable in which the result of the POST request will be stored. It should be a variable that will receive the server's response. Command Flow Build the Request: Uses the provided URL, query string, headers, and body to construct the POST request. Send the Request: Sends the POST request to the specified server. Store the Result: Saves the server's response in the variable specified by o_result. Example Usage Suppose you want to send a POST request to https://api.example.com/data, with a query string userId=123, headers including Content-Type: application/json, and a body with JSON data. // Define variables url = \"https://api.example.com/data\" querystring = \"userId=123\" headers = {\"Content-Type\": \"application/json\"} body = '{\"name\": \"Alice\", \"age\": 30}' response = '' // Call the command to perform the POST request RequestPost(url, querystring, headers, body, response) // Return the request result via addResult addResult(response) In this example, the RequestPost() command will send a POST request to https://api.example.com/data with the provided query string, headers, and body. The server's response will be stored in the response variable, and this variable will be returned via addResult(response). The result of the request will be included in the final response. ormCreateTable() The ormCreateTable() command creates a new table in a database using the specified ORM (Object-Relational Mapping). This command defines the columns of the table and their data types, and stores a reference to the created table in a destination variable. Parameters fields Type: value Description: A string containing the names of the table columns, separated by commas. Each column name should correspond to a field in the table. fieldsType Type: value Description: A string containing the data types for each column, separated by commas. The data types should be in the same order as the column names in fields. dbaseName Type: value Description: The name of the database where the table will be created. It should be a string indicating the target database. varTarget Type: variable Description: The variable in which the reference to the created table will be stored. It should be a variable that will receive the reference to the new table. Command Flow Define the Table: Uses the column names (fields) and their data types (fieldsType) to define the structure of the new table. Create the Table: Creates the table in the database specified by dbaseName using the provided definition. Store the Result: Saves the reference to the created table in the variable specified by varTarget. Example Usage Suppose you want to create a table called users in a database called myDatabase, with two columns: username of type VARCHAR and age of type INTEGER. // Define variables fields = \"username,age\" fieldsType = \"VARCHAR,INTEGER\" dbaseName = \"myDatabase\" tableReference = '' // Call the command to create the table ormCreateTable(fields, fieldsType, dbaseName, tableReference) // Return the reference to the created table via addResult addResult(tableReference) In this example, the ormCreateTable() command will create a table in the myDatabase database with the specified columns and data types. The reference to the new table will be stored in the tableReference variable, and this variable will be returned via addResult(tableReference). The output will include the reference to the created table. ormCheckTable() The ormCheckTable() command checks for the existence of a table in a specific database and stores the result in a destination variable. This command is useful for verifying if a table already exists before attempting further operations on it. Parameters dbaseName Type: value Description: The name of the database in which the table's existence should be checked. It should be a string indicating the database to check. varTarget Type: variable Description: The variable in which the result of the check will be stored. It should be a variable that will receive a value indicating whether the table exists or not. Command Flow Check Existence: Accesses the database specified by dbaseName to verify if the requested table exists. Store the Result: Saves the result of the check in the variable specified by varTarget. The stored value will indicate whether the table exists (True or False). Example Usage Suppose you want to check if a table called users exists in a database called myDatabase. // Define variables dbaseName = \"myDatabase\" tableExists = '' // Call the command to check the existence of the table ormCheckTable(dbaseName, tableExists) // Return the result of the check via addResult addResult(tableExists) In this example, the ormCheckTable() command will check for the existence of the users table in the myDatabase database. The result of the check (whether the table exists or not) will be stored in the tableExists variable, and this variable will be returned via addResult(tableExists). The output will reflect whether the table exists (True) or not (False). ormAccessUpdate() The ormAccessUpdate() command updates records in a database table based on the provided selection criteria. This command modifies the values of specified fields in a database using the corresponding values from variables. Parameters fields Type: variable Description: A string containing the names of the fields to be updated. The field names should be separated by commas. fieldsValuesVariables Type: variable Description: A string containing the names of the variables holding the new values for the specified fields. The variable names should be separated by commas, in the same order as the fields in fields. dbase Type: variable Description: The name of the database where the table to be updated is located. It should be a variable containing the name of the database. selector Type: variable Description: A condition to select the records to be updated. It should be a string specifying the selection criteria in SQL format, such as id = 1. varTarget Type: variable Description: The variable in which the result of the update operation will be stored. It should be a variable that will receive a value indicating whether the update was successful or not. Command Flow Define Fields and Values: Uses the field names (fields) and the variables with the values to be updated (fieldsValuesVariables) to define which records should be modified and with what data. Select Records: Uses the condition provided in selector to identify the records to be updated. Update the Database: Performs the update in the database specified by dbase, applying the changes to the records that meet the selector condition. Store the Result: Saves the result of the update operation in the variable specified by varTarget. The stored value will indicate whether the update was successful (True) or failed (False). Example Usage Suppose you want to update the age field to 31 for the user with id equal to 1 in a database called myDatabase. // Define variables fields = \"age\" fieldsValuesVariables = \"newAge\" dbase = \"myDatabase\" selector = \"id = 1\" updateSuccess = '' // Define the variable holding the new value newAge = 31 // Call the command to update the record ormAccessUpdate(fields, fieldsValuesVariables, dbase, selector, updateSuccess) // Return the result of the update via addResult addResult(updateSuccess) In this example, the ormAccessUpdate() command will update the age field in the myDatabase database for the record where id = 1. The new value for age is 31, stored in the newAge variable. The updateSuccess variable will store the result of the operation (whether it was successful or not), and this variable will be returned via addResult(updateSuccess). ormAccessSelect() The ormAccessSelect() command retrieves records from a table in a database based on the provided selection criteria. This command selects the desired fields and stores the results in a target variable. Parameters fields Type: variable Description: A string containing the names of the fields to be retrieved. The field names should be separated by commas. dbase Type: variable Description: The name of the database from which records should be retrieved. It must be a variable containing the name of the database. selector Type: variable Description: A condition to select the records to be retrieved. It must be a string specifying the selection criteria in SQL format, such as id = 1. varTarget Type: variable Description: The variable in which the query results will be stored. It must be a variable that will receive a list of dictionaries, each representing a retrieved record. Command Flow Defining the Fields: Use the field names (fields) to specify which data should be retrieved. Selecting Records: Use the condition provided in selector to identify which records should be selected from the database. Retrieving Data: Access the database specified by dbase and retrieve the records that meet the selector condition, including only the specified fields. Storing the Result: Save the query results in the variable specified by varTarget. The stored value will be a list of dictionaries, where each dictionary represents a retrieved record with the requested fields. Example Usage Suppose you want to retrieve the username field for all users where age is greater than 25 from a database called myDatabase. // Define variables fields = \"username\" dbase = \"myDatabase\" selector = \"age > 25\" usersList = '' // Call the command to retrieve the records ormAccessSelect(fields, dbase, selector, usersList) // Return the query results via addResult addResult(usersList) In this example, the ormAccessSelect() command will retrieve the username field for all users in the myDatabase database where age is greater than 25. The results will be stored in the usersList variable, and this variable will be returned via addResult(usersList). The output will be a list of dictionaries, each representing a user whose username has been retrieved. ormAccessInsert() The ormAccessInsert() command inserts a new record into a database table using the provided values for the fields. This command defines the fields and their corresponding values, and stores the result of the operation in a target variable. Parameters fields Type: variable Description: A string containing the names of the fields into which the values will be inserted. The field names should be separated by commas. fieldsValuesVariables Type: variable Description: A string containing the names of the variables that hold the values to be inserted into the specified fields. The variable names should be separated by commas, in the same order as the fields in fields. dbase Type: variable Description: The name of the database where the table into which the new record should be inserted is located. It must be a variable containing the name of the database. varTarget Type: variable Description: The variable in which the result of the insertion operation will be stored. It must be a variable that will receive a value indicating whether the insertion was successful or not. Command Flow Defining the Fields and Values: Use the field names (fields) and the variables with the values to be inserted (fieldsValuesVariables) to define what data should be inserted. Inserting into the Database: Perform the insertion of the new record into the database specified by dbase, using the provided values. Storing the Result: Save the result of the insertion operation in the variable specified by varTarget. The stored value will indicate whether the insertion was successful (True) or failed (False). Example Usage Suppose you want to insert a new record into a table called users in a database called myDatabase, with values for username and age coming from the variables newUsername and newAge. // Define variables fields = \"username,age\" fieldsValuesVariables = \"newUsername,newAge\" dbase = \"myDatabase\" insertSuccess = '' // Define the variables with the new values newUsername = \"Alice\" newAge = 31 // Call the command to insert the new record ormAccessInsert(fields, fieldsValuesVariables, dbase, insertSuccess) // Return the result of the insertion via addResult addResult(insertSuccess) In this example, the ormAccessInsert() command will insert a new record into the myDatabase database in the users table. The values for username and age are provided by the newUsername and newAge variables. The insertSuccess variable will store the result of the operation (whether it was successful or not), and this variable will be returned via addResult(insertSuccess). The output will reflect whether the insertion was successful (True) or failed (False). ormAI() The ormAI() command uses an artificial intelligence model to convert a natural language query into an SQL statement, which is then executed against a database. This command processes a natural language query to generate an SQL statement that is executed on the table specified in the source parameter, and stores the result in a target variable. Parameters prompt Type: variable Description: A string in natural language that describes the query to be made. For example, \"get the value of the row with id 5\". source Type: variable Description: The name of the table on which the generated query should be executed. It must be a variable containing the name of the table in the database. TargetVariable Type: variable Description: The variable in which the result of the query will be stored. It must be a variable that will receive the result of the generated and executed SQL query. Command Flow Generating SQL Query: Use the artificial intelligence model to convert the prompt into an SQL statement. For example, if the prompt is \"get the value of the row with id 5\", the AI will generate the SQL query SELECT * FROM source WHERE id = 5;. Executing the Query: Execute the generated SQL statement on the table specified in source. Storing the Result: Save the result of the query execution in the variable specified by TargetVariable. The result will be the dataset retrieved by the executed SQL statement. Example Usage Suppose you want to retrieve all the data from the row with id equal to 5 from a table called users. // Define variables prompt = \"get the value of the row with id 5\" source = \"users\" queryResult = '' // Call the command to process the query ormAI(prompt, source, queryResult) // Return the query result via addResult addResult(queryResult) In this example, the ormAI() command will convert the prompt into an SQL query: SELECT * FROM users WHERE id = 5;. This query will be executed on the users table, and the results will be stored in the queryResult variable. The queryResult variable will be returned via addResult(queryResult). The output will be the dataset retrieved by the executed SQL statement. functionAI() The functionAI() command uses an artificial intelligence model to convert a natural language description of a function or process into a code implementation, which is then executed and returns the result. This command converts a description provided in prompt into a function that operates on the data of the table specified in source, and stores the result in a target variable. Parameters prompt Type: variable Description: A string in natural language that describes the process or function to be executed. For example, \"calculate the average of the salary column\". source Type: variable Description: The name of the table on which the generated function should be executed. It must be a variable containing the name of the table in the database. TargetVariable Type: variable Description: The variable in which the result of the executed function or process will be stored. It must be a variable that will receive the result of the generated and executed code. Command Flow Generating Code: Use the artificial intelligence model to convert the prompt into a code implementation. For example, if the prompt is \"calculate the average of the salary column\", the AI will generate the code necessary to calculate the average of that column. Executing the Code: Execute the generated code on the table specified in source. Storing the Result: Save the result of the code execution in the variable specified by TargetVariable. The result will be the calculated value or the dataset produced by the executed code. Example Usage Suppose you want to calculate the average of the salary column in a table called employees. // Define variables prompt = \"calculate the average of the salary column\" source = \"employees\" averageSalary = '' // Call the command to process the function functionAI(prompt, source, averageSalary) // Return the result of the function via addResult addResult(averageSalary) In this example, the functionAI() command will convert the prompt into a code implementation to calculate the average of the salary column in the employees table. The result of the calculation will be stored in the averageSalary variable, and this variable will be returned via addResult(averageSalary). The output will be the calculated average of the salary column.\n", + "\n", + "[2] id=chunk-2 source=21_Persistance_connectors_orm.txt\n", + "SECTION V: Persistence, Connectors, and Native ORM AVAP is designed to be database-agnostic. It enables data manipulation through three layers: the universal connector, simplified ORM commands, and direct SQL execution. 5.1 The Universal Connector (avapConnector) The avapConnector command is the entry point for any external integration. It uses a Connection Token system (Base64) that encapsulates configuration details (host, port, credentials, driver) to keep code clean and secure. Interface connector_variable = avapConnector(\"BASE64_TOKEN\") Connector Object Capabilities Once instantiated, the variable behaves as an object with dynamic methods: Database Connectors: Expose the .query(sql_string) method, which returns objects or lists depending on the result set. API Connectors (Twilio, Slack, etc.): Expose native service methods (e.g., .send_sms()). Example: Dynamic Assignment with Connectors // Instantiate the connection db = avapConnector(\"REJfQ09OTkVDVE9SM...\") // Execute query and use Section I dynamic evaluation users = db.query(\"SELECT * FROM users\") first_admin = users[0].name if users[0].role == 'admin' else 'N/A' addResult(first_admin) 5.2 Native ORM Layer (ormCheckTable / ormDirect) For quick operations on the local or default database cluster, AVAP provides system-level commands that do not require prior instantiation. 5.2.1 ormCheckTable Verifies the existence of a database structure. It is critical for installation scripts or automated migrations. Interface: ormCheckTable(table_name, target_var) Response: target_var receives the string values \"True\" or \"False\". 5.2.2 ormDirect Executes SQL statements directly. Unlike .query(), it is optimized for statements that do not necessarily return rows (such as INSERT, UPDATE, or CREATE TABLE). Interface: ormDirect(statement, target_var) Interpolation Usage Example: ormDirect(\"UPDATE users SET login = '%s' WHERE id = %s\" % (now, id), result) 5.3 Data Access Abstraction (Implicit Commands) AVAP includes specialized commands for common CRUD operations, reducing the need to write manual SQL and mitigating injection risks. ormAccessSelect Performs filtered queries returning a list-of-objects structure. Syntax: ormAccessSelect(table, filters, target) ormAccessInsert / ormAccessUpdate Manages data persistence. If used on an object that already has an ID, Update synchronizes changes; otherwise, Insert creates the record. 5.4 Dynamic Query Formatting (Injection Prevention) As detailed in Section I, the AVAP engine processes SQL strings before sending them to the database engine. The official recommendation is to always use interpolation with the % operator to ensure proper handling of data types (Strings vs Integers) by the driver. Recommended Secure Pattern sql = \"SELECT * FROM %s WHERE status = '%s'\" % (table_name, recovered_status) res = db.query(sql) 5.5 Cryptographic Security Integration (encodeSHA256) Within the persistence flow, AVAP provides native tools to secure sensitive data before it is written to disk. Interface encodeSHA256(source_text, target_variable) Complete Registration Flow (Final Example) This example integrates Sections I, II, III, and V: // II: Input capture addParam(\"pass\", p) addParam(\"user\", u) // I & V: Processing and security encodeSHA256(p, secure_pass) // V: Insertion sql = \"INSERT INTO users (username, password) VALUES ('%s', '%s')\" % (u, secure_pass) ormDirect(sql, db_result) // III & II: Response if(db_result, \"Success\", \"=\") addVar(msg, \"User created\") addResult(msg) end() Examples 1. Connector Instantiation Code snippet my_db = avapConnector(\"VE9LRU5fREVCX0RFU0FSUk9MTE8=\") 2. Record Retrieval Code snippet rows = my_db.query(\"SELECT id, name FROM users\") addResult(rows) 3. Direct Command Execution Code snippet ormDirect(\"TRUNCATE TABLE temp_cache\", status) 4. Structure Verification Code snippet ormCheckTable(\"inventory\", exists) if(exists, \"False\", \"==\") ormDirect(\"CREATE TABLE inventory...\", r) end() 5. Secure Update (Interpolation) Code snippet sql = \"UPDATE users SET login_count = %s WHERE email = '%s'\" % (count, email) ormDirect(sql, res) 6. JSON/DB Object Navigation Code snippet found_id = query_result[0].id addResult(found_id) 7. ORM Select with Filter Code snippet ormAccessSelect(\"orders\", {\"status\": \"pending\"}, list_result) addResult(list_result) 8. Processing Database Results Code snippet records = db.query(\"SELECT...\") startLoop(i, 0, len(records)) name = records[i].name endLoop() 9. Cryptographic Persistence Code snippet encodeSHA256(password_raw, hashed) ormDirect(\"INSERT INTO logins (hash) VALUES ('%s')\" % hashed, r) 10. Third-Party Connector (e.g., Slack) Code snippet slack_api = avapConnector(\"U0xBQ0tfQVBJX1RPS0VO\")\n", + "\n", + "[3] id=chunk-3 source=22_System_utilities_transformation.txt\n", + "SECTION VI: System Utilities and Transformation This section documents the native commands for advanced string manipulation, precise time handling, and dynamic data generation. 6.1 Time and Date Management (getDateTime / stampToDatetime) AVAP handles time in two formats: Epoch/Timestamp (numeric): Ideal for calculations. Formatted Datetime (string): Ideal for human readability and database storage. 6.1.1 getDateTime Generates the current time with high precision. Interface: getDateTime(format, timeDelta, timeZone, targetVar) Parameters format: Example: \"%Y-%m-%d %H:%M:%S\". If left empty, returns the current Epoch timestamp. timeDelta: Seconds to add (positive) or subtract (negative). Particularly useful for calculating token expiration times. timeZone: Time zone region (e.g., \"Europe/Madrid\"). 6.1.2 stampToDatetime Converts a numeric value (Unix Timestamp) into a human-readable string. Interface: stampToDatetime(timestamp, format, offset, targetVar) Common Use Case: Formatting dates retrieved from the database (Section V) before sending them to the client (Section II). 6.2 Advanced String Manipulation (replace / randomString) 6.2.1 replace Allows text cleaning and transformation. Essential when receiving client data that requires sanitization. Interface: replace(sourceText, oldText, newText, targetVar) Example Use Case: Removing spaces or unwanted characters from a username before executing a SQL query. 6.2.2 randomString Generates secure random alphanumeric strings. Interface: randomString(length, targetVar) Applications: Temporary password generation Session ID creation Unique file name generation 6.3 Security and Hash Operations (encodeSHA256) Although previously mentioned in the persistence section, this is fundamentally a data transformation utility. Mechanics Deterministic one-way function. AVAP uses an optimized implementation ensuring that the same input always produces the same hash. This enables secure login comparisons without storing or exposing the actual password. 6.4 The Return Command (return) Within functions and execution flows, return not only stops execution but can also inject the result of a subroutine back into the main flow. Complete Utility Flow Example // 1. Generate a temporary token randomString(16, token_raw) // 2. Calculate expiration (within 1 hour = 3600 seconds) getDateTime(\"%Y-%m-%d %H:%M:%S\", 3600, \"UTC\", expiration_date) // 3. Format a system message using Section I message = \"Your token %s expires on %s\" % (token_raw, expiration_date) // 4. Send to client (Section II) addResult(message) 6.5 Common Format Tokens (Cheat Sheet) Token Description Example %Y Full year 2026 %m Month (01–12) 02 %d Day (01–31) 23 %H Hour (00–23) 21 %M Minute (00–59) 45 Examples 1. Unix Timestamp Retrieval Code snippet getDateTime(\"\", 0, \"UTC\", now) addResult(now) 2. Database-Formatted Date Code snippet getDateTime(\"%Y-%m-%d %H:%M:%S\", 0, \"Europe/Madrid\", sql_date) addResult(sql_date) 3. Expiration Calculation (1 Day) Code snippet getDateTime(\"\", 86400, \"UTC\", expires_at) addResult(expires_at) 4. Timestamp to Readable Conversion Code snippet stampToDatetime(1708726162, \"%d/%m/%Y\", 0, human_date) addResult(human_date) 5. String Cleaning (Replace) Code snippet replace(\"REF_1234_OLD\", \"OLD\", \"NEW\", updated_ref) addResult(updated_ref) 6. Random Token Generator Code snippet randomString(32, security_token) addResult(security_token) 7. SHA256 Hash for Integrity Code snippet encodeSHA256(\"payload_data\", checksum) addResult(checksum)\n", + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "Suppose you want to create a table called users in a database called myDatabase, with two columns: username of type VARCHAR and age of type INTEGER. How would you do that in AVAP?\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "To create a table named `users` in your database `myDatabase` with two columns (`username` as VARCHAR and `age` as INTEGER) using AVAP, you would use the `ormDirect` command to execute SQL statements directly. Here is how you would do it:\n", + "\n", + "```avap\n", + "ormDirect(\"CREATE TABLE users (username VARCHAR(255), age INTEGER)\", myDatabase)\n", + "```\n", + "\n", + "### Explanation:\n", + "- **`ormDirect`**: Executes SQL statements directly without requiring prior instantiation of a database connection object.\n", + "- **SQL Statement**: `CREATE TABLE users (username VARCHAR(255), age INTEGER)` defines the table structure.\n", + " - `VARCHAR(255)` specifies that the `username` column can hold variable-length strings up to 255 characters.\n", + " - `INTEGER` specifies that the `age` column will store integer values.\n", + "- **Target Variable**: `myDatabase` is passed as the target variable, which AVAP uses to identify the database context for this operation.\n", + "\n", + "### Alternative: Using ORM Commands (If Supported)\n", + "If your environment supports ORM-style commands, you could also use:\n", + "\n", + "```avap\n", + "ormAccessInsert(\"users\", \"username VARCHAR(255), age INTEGER\")\n", + "ormDirect(\"INSERT INTO users (username, age) VALUES ('test', 30)\", myDatabase)\n", + "```\n", + "\n", + "However, for a direct table creation using `ormDirect`, the first approach is recommended.\n", + "\n", + "### Important Notes:\n", + "- Ensure that your database driver supports the SQL syntax used in this command.\n", + "- If you need to specify the database name explicitly in the SQL statement (e.g., `myDatabase.users`), you can modify the SQL string accordingly.\n", + "- For more complex operations like inserting or updating data, use `ormDirect` with appropriate parameters and interpolation for security.\n", + "\n", + "This approach ensures that your table is created correctly within the AVAP framework while maintaining database safety and consistency.\n" + ] + } + ], + "source": [ + "a = stream_graph_updates(user_input, guided_graph)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6f512362", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "assistance-engine", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/research/agents/n00 langgraph_agent_simple.ipynb b/research/agents/n00 langgraph_agent_simple.ipynb new file mode 100644 index 0000000..6af03de --- /dev/null +++ b/research/agents/n00 langgraph_agent_simple.ipynb @@ -0,0 +1,401 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9f97dd1e", + "metadata": {}, + "source": [ + "# Libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "9e974df6", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from typing import TypedDict, List, Optional, Annotated\n", + "from IPython.display import Image, display\n", + "\n", + "from langchain_core.documents import Document\n", + "from langchain_core.messages import BaseMessage, SystemMessage\n", + "from langchain_core.tools import tool\n", + "from langgraph.checkpoint.memory import InMemorySaver\n", + "from langgraph.graph.message import add_messages\n", + "from langchain_ollama import ChatOllama, OllamaEmbeddings\n", + "from langchain_elasticsearch import ElasticsearchStore\n", + "from langgraph.graph import StateGraph, END\n", + "from langgraph.prebuilt import ToolNode\n", + "from langfuse import observe, get_client" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30edcecc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DEBUG: LANGFUSE_HOST from env = http://45.77.119.180\n", + "DEBUG: Langfuse client base_url = NOT SET\n" + ] + } + ], + "source": [ + "ES_URL = os.getenv(\"ELASTICSEARCH_LOCAL_URL\")\n", + "INDEX_NAME = os.getenv(\"ELASTICSEARCH_INDEX\")\n", + "BASE_URL = os.getenv(\"LLM_BASE_LOCAL_URL\")\n", + "MODEL_NAME = os.getenv(\"OLLAMA_MODEL_NAME\")\n", + "LANGFUSE_PUBLIC_KEY = os.getenv(\"LANGFUSE_PUBLIC_KEY\")\n", + "LANGFUSE_SECRET_KEY = os.getenv(\"LANGFUSE_SECRET_KEY\")\n", + "LANGFUSE_HOST = os.getenv(\"LANGFUSE_HOST\")\n", + "\n", + "print(f\"DEBUG: LANGFUSE_HOST from env = {LANGFUSE_HOST}\")\n", + "\n", + "# Initialize Langfuse client\n", + "langfuse = get_client()\n", + "\n", + "# Print actual client configuration\n", + "print(f\"DEBUG: Langfuse client base_url = {langfuse.base_url if hasattr(langfuse, 'base_url') else 'NOT SET'}\")\n", + "\n", + "embeddings = OllamaEmbeddings(base_url=BASE_URL, model=MODEL_NAME)\n", + "llm = ChatOllama(base_url=BASE_URL, model=MODEL_NAME)\n", + "\n", + "vector_store = ElasticsearchStore(\n", + " es_url=ES_URL,\n", + " index_name=INDEX_NAME,\n", + " embedding=embeddings,\n", + " query_field=\"text\",\n", + " vector_query_field=\"vector\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "ad98841b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Langfuse Configuration:\n", + " Host: http://45.77.119.180\n", + " Public Key: **********\n", + " Secret Key: **********\n", + "\n", + "✓ All Langfuse environment variables are set\n", + " Tracing will be sent to Langfuse when you run the agent\n" + ] + } + ], + "source": [ + "print(f\"Langfuse Configuration:\")\n", + "print(f\" Host: {LANGFUSE_HOST}\")\n", + "print(f\" Public Key: {'*' * 10 if LANGFUSE_PUBLIC_KEY else 'NOT SET'}\")\n", + "print(f\" Secret Key: {'*' * 10 if LANGFUSE_SECRET_KEY else 'NOT SET'}\")\n", + "\n", + "if all([LANGFUSE_HOST, LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY]):\n", + " print(\"\\n✓ All Langfuse environment variables are set\")\n", + " print(\" Tracing will be sent to Langfuse when you run the agent\")\n", + "else:\n", + " print(\"\\n⚠ Some Langfuse variables are missing - tracing may not work\")\n", + " print(f\" Set LANGFUSE_HOST, LANGFUSE_PUBLIC_KEY, and LANGFUSE_SECRET_KEY\")" + ] + }, + { + "cell_type": "markdown", + "id": "873ea2f6", + "metadata": {}, + "source": [ + "### State" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "5f8c88cf", + "metadata": {}, + "outputs": [], + "source": [ + "class AgentState(TypedDict):\n", + " messages: Annotated[list, add_messages]" + ] + }, + { + "cell_type": "markdown", + "id": "1d60c120", + "metadata": {}, + "source": [ + "### Tools" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "f9359747", + "metadata": {}, + "outputs": [], + "source": [ + "def format_context(docs: List[Document]) -> str:\n", + " chunks: List[str] = []\n", + " for i, doc in enumerate(docs, 1):\n", + " title = (doc.metadata or {}).get(\"title\", \"Untitled\")\n", + " source_id = (doc.metadata or {}).get(\"id\", f\"chunk-{i}\")\n", + " text = doc.page_content or \"\"\n", + " chunks.append(f\"[{i}] id={source_id} title={title}\\n{text}\")\n", + " return \"\\n\\n\".join(chunks)\n", + "\n", + "\n", + "@tool(\"retrieve\", return_direct=False)\n", + "def retrieve(query: str, k: int = 4, title_filter: Optional[str] = None) -> str:\n", + " \"\"\"Retrieve relevant context from Elasticsearch for a given query.\"\"\"\n", + " search_kwargs = {\"k\": k}\n", + " if title_filter:\n", + " search_kwargs[\"filter\"] = {\"term\": {\"metadata.title.keyword\": title_filter}}\n", + "\n", + " retriever = vector_store.as_retriever(\n", + " search_type=\"similarity\",\n", + " search_kwargs=search_kwargs,\n", + " )\n", + " docs = retriever.invoke(query)\n", + " return format_context(docs)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "e5247ab9", + "metadata": {}, + "outputs": [], + "source": [ + "def should_continue(state: AgentState) -> str:\n", + " last = state[\"messages\"][-1]\n", + " # If the model requested tool calls, go execute them\n", + " if getattr(last, \"tool_calls\", None):\n", + " return \"tools\"\n", + " return \"end\"" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "a644f6fa", + "metadata": {}, + "outputs": [], + "source": [ + "tools = [retrieve]\n", + "tool_node = ToolNode(tools)\n", + "memory = InMemorySaver()" + ] + }, + { + "cell_type": "markdown", + "id": "395966e2", + "metadata": {}, + "source": [ + "### Agent" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "36d0f54e", + "metadata": {}, + "outputs": [], + "source": [ + "def agent(state: AgentState) -> AgentState:\n", + " messages: List[BaseMessage] = state[\"messages\"]\n", + "\n", + " system = SystemMessage(\n", + " content=(\n", + " \"You are a helpful assistant. You must use the tools provided to respond.\\n\"\n", + " \"If you don't have enough info, ask a precise follow-up question.\"\n", + " )\n", + " )\n", + "\n", + " # IMPORTANT: bind tools so the model can emit tool calls\n", + " # Also bind the langfuse handler for tracing\n", + " model = llm.bind_tools(tools)\n", + "\n", + " resp = model.invoke([system, *messages], config={\"callbacks\": [langfuse_handler]})\n", + " return {\"messages\": [*messages, resp]}" + ] + }, + { + "cell_type": "markdown", + "id": "ef55bca3", + "metadata": {}, + "source": [ + "### Graph" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "fae46a58", + "metadata": {}, + "outputs": [], + "source": [ + "graph = StateGraph(AgentState)\n", + "graph.add_node(\"agent\", agent)\n", + "graph.add_node(\"tools\", tool_node)\n", + "\n", + "graph.set_entry_point(\"agent\")\n", + "graph.add_conditional_edges(\"agent\", should_continue, {\"tools\": \"tools\", \"end\": END})\n", + "graph.add_edge(\"tools\", \"agent\")\n", + "\n", + "agent_graph = graph.compile(checkpointer=memory)" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "2fec3fdb", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "try:\n", + " display(Image(agent_graph.get_graph().draw_mermaid_png()))\n", + "except Exception:\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "1e9aff05", + "metadata": {}, + "source": [ + "### Test" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "a7f4fbf6", + "metadata": {}, + "outputs": [], + "source": [ + "user_input = \"What does vertical farming proposes?\"" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "8569cf39", + "metadata": {}, + "outputs": [], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"1\"}, \n", + " \"callbacks\": [langfuse_handler],\n", + " \"run_name\": \"rag-local-test\"}\n", + "\n", + "\n", + "@observe(name=\"agent-graph-stream\")\n", + "def stream_graph_updates(user_input: str):\n", + " for event in agent_graph.stream(\n", + " {\"messages\": [{\"role\": \"user\", \"content\": user_input}]},\n", + " config=config,\n", + " stream_mode=\"values\",\n", + " ):\n", + " event[\"messages\"][-1].pretty_print()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53b89690", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting agent...\n", + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "What does vertical farming proposes?\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n", + "Failed to export span batch code: 404, reason:
\"Langfuse

Loading ...

\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "\n", + "Okay, the user is asking what vertical farming proposes. First, I need to recall what vertical farming is. From what I remember, vertical farming is a method of growing crops in stacked layers, often in controlled environments. It's a form of hydroponics or aquaponics. The idea is to maximize space efficiency by using vertical structures. \n", + "\n", + "But wait, maybe I should check if there's any specific information I'm missing. The user might be looking for details about the technology involved, like using LED lights, hydroponics, or aquaponics. Also, vertical farms can reduce the need for land, water, and pesticides. \n", + "\n", + "I should present this information clearly. Let me make sure to mention the key aspects: stacked layers, controlled environment, space efficiency, and sustainability. That should answer the question comprehensively.\n", + "\n", + "\n", + "Vertical farming is a method of growing crops in stacked layers (like vertical gardens) in controlled environments, often using hydroponics or aquaponics. It maximizes space efficiency, reduces water and land use, and promotes sustainability by minimizing resource consumption.\n", + "\n", + "Flushing traces to Langfuse...\n", + "✓ Traces flushed (check your Langfuse dashboard at http://45.77.119.180)\n" + ] + } + ], + "source": [ + "print(\"Starting agent...\")\n", + "stream_graph_updates(user_input)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5bfbf18", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "assistance-engine", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/research/embeddings/Embedding model selection.pdf b/research/embeddings/Embedding model selection.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d295567fe19db0b854468ad63948412fd9157034 GIT binary patch literal 105074 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�-E= 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

j2$QFBbT*@5eWDQHh`beT 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

NXGE8$2rcoU{CwP=?P$50!0{ZMpihC>=9X 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 aW~d=i|B9aT?^;7gQw4<}ua!Tl{eJ-_ literal 0 HcmV?d00001 diff --git a/research/embeddings/embedding_eval_results/beir_CodeXGlue_results.json b/research/embeddings/embedding_eval_results/beir_CodeXGlue_results.json new file mode 100644 index 0000000..2763405 --- /dev/null +++ b/research/embeddings/embedding_eval_results/beir_CodeXGlue_results.json @@ -0,0 +1,54 @@ +{ + "qwen3-0.6B-emb:latest": { + "NDCG": { + "NDCG@1": 0.94971, + "NDCG@3": 0.96956, + "NDCG@5": 0.97166, + "NDCG@10": 0.97342 + }, + "MAP": { + "MAP@1": 0.94971, + "MAP@3": 0.96504, + "MAP@5": 0.9662, + "MAP@10": 0.96694 + }, + "Recall": { + "Recall@1": 0.94971, + "Recall@3": 0.98251, + "Recall@5": 0.98761, + "Recall@10": 0.99297 + }, + "Precision": { + "P@1": 0.94971, + "P@3": 0.3275, + "P@5": 0.19752, + "P@10": 0.0993 + } + }, + "qwen2.5:1.5b": { + "NDCG": { + "NDCG@1": 0.00031, + "NDCG@3": 0.00061, + "NDCG@5": 0.00086, + "NDCG@10": 0.00118 + }, + "MAP": { + "MAP@1": 0.00031, + "MAP@3": 0.00051, + "MAP@5": 0.00065, + "MAP@10": 0.00078 + }, + "Recall": { + "Recall@1": 0.00031, + "Recall@3": 0.00088, + "Recall@5": 0.00151, + "Recall@10": 0.0025 + }, + "Precision": { + "P@1": 0.00031, + "P@3": 0.00029, + "P@5": 0.0003, + "P@10": 0.00025 + } + } +} \ No newline at end of file diff --git a/research/embeddings/embedding_eval_results/beir_Scifact_results.json b/research/embeddings/embedding_eval_results/beir_Scifact_results.json new file mode 100644 index 0000000..9518098 --- /dev/null +++ b/research/embeddings/embedding_eval_results/beir_Scifact_results.json @@ -0,0 +1,62 @@ +{ + "qwen3-0.6B-emb:latest": { + "NDCG": { + "NDCG@1": 0.56333, + "NDCG@3": 0.64367, + "NDCG@5": 0.66577, + "NDCG@10": 0.68551, + "NDCG@100": 0.71285 + }, + "MAP": { + "MAP@1": 0.52994, + "MAP@3": 0.6117, + "MAP@5": 0.62815, + "MAP@10": 0.6383, + "MAP@100": 0.64466 + }, + "Recall": { + "Recall@1": 0.52994, + "Recall@3": 0.7035, + "Recall@5": 0.75967, + "Recall@10": 0.81611, + "Recall@100": 0.94 + }, + "Precision": { + "P@1": 0.56333, + "P@3": 0.25889, + "P@5": 0.17067, + "P@10": 0.093, + "P@100": 0.0107 + } + }, + "qwen2.5:1.5b": { + "NDCG": { + "NDCG@1": 0.02333, + "NDCG@3": 0.03498, + "NDCG@5": 0.0404, + "NDCG@10": 0.04619, + "NDCG@100": 0.07768 + }, + "MAP": { + "MAP@1": 0.02083, + "MAP@3": 0.03083, + "MAP@5": 0.03375, + "MAP@10": 0.03632, + "MAP@100": 0.04123 + }, + "Recall": { + "Recall@1": 0.02083, + "Recall@3": 0.04417, + "Recall@5": 0.0575, + "Recall@10": 0.07417, + "Recall@100": 0.23144 + }, + "Precision": { + "P@1": 0.02333, + "P@3": 0.01556, + "P@5": 0.01267, + "P@10": 0.00833, + "P@100": 0.00277 + } + } +} \ No newline at end of file diff --git a/research/embeddings/embedding_eval_results/beir_cosqa_results.json b/research/embeddings/embedding_eval_results/beir_cosqa_results.json new file mode 100644 index 0000000..d44b968 --- /dev/null +++ b/research/embeddings/embedding_eval_results/beir_cosqa_results.json @@ -0,0 +1,62 @@ +{ + "qwen3-0.6B-emb:latest": { + "NDCG": { + "NDCG@1": 0.174, + "NDCG@3": 0.27374, + "NDCG@5": 0.33509, + "NDCG@10": 0.39086, + "NDCG@100": 0.45099 + }, + "MAP": { + "MAP@1": 0.174, + "MAP@3": 0.247, + "MAP@5": 0.2808, + "MAP@10": 0.30466, + "MAP@100": 0.31702 + }, + "Recall": { + "Recall@1": 0.174, + "Recall@3": 0.352, + "Recall@5": 0.502, + "Recall@10": 0.67, + "Recall@100": 0.952 + }, + "Precision": { + "P@1": 0.174, + "P@3": 0.11733, + "P@5": 0.1004, + "P@10": 0.067, + "P@100": 0.00952 + } + }, + "qwen2.5:1.5b": { + "NDCG": { + "NDCG@1": 0.0, + "NDCG@3": 0.0, + "NDCG@5": 0.0, + "NDCG@10": 0.0, + "NDCG@100": 0.0021 + }, + "MAP": { + "MAP@1": 0.0, + "MAP@3": 0.0, + "MAP@5": 0.0, + "MAP@10": 0.0, + "MAP@100": 0.00043 + }, + "Recall": { + "Recall@1": 0.0, + "Recall@3": 0.0, + "Recall@5": 0.0, + "Recall@10": 0.0, + "Recall@100": 0.01 + }, + "Precision": { + "P@1": 0.0, + "P@3": 0.0, + "P@5": 0.0, + "P@10": 0.0, + "P@100": 0.0001 + } + } +} \ No newline at end of file diff --git a/research/embeddings/n00 Beir Analysis CodeXGlue.ipynb b/research/embeddings/n00 Beir Analysis CodeXGlue.ipynb new file mode 100644 index 0000000..a4bc78f --- /dev/null +++ b/research/embeddings/n00 Beir Analysis CodeXGlue.ipynb @@ -0,0 +1,333 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "66cbbaf8", + "metadata": {}, + "source": [ + "# Libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "c01c19dc", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Dict, List, Union\n", + "import numpy as np\n", + "from langchain_ollama import OllamaEmbeddings\n", + "from beir.datasets.data_loader import GenericDataLoader\n", + "from beir.retrieval.search.dense import DenseRetrievalExactSearch\n", + "from beir.retrieval.evaluation import EvaluateRetrieval\n", + "from beir import util\n", + "import json\n", + "from datasets import load_dataset" + ] + }, + { + "cell_type": "markdown", + "id": "ac011c1c", + "metadata": {}, + "source": [ + "# Utils" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "b83e7900", + "metadata": {}, + "outputs": [], + "source": [ + "class BEIROllamaEmbeddings:\n", + " \"\"\"\n", + " Adapter that makes LangChain's OllamaEmbeddings compatible with BEIR.\n", + " \"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " base_url: str,\n", + " model: str,\n", + " batch_size: int = 64,\n", + " ) -> None:\n", + " self.batch_size = batch_size\n", + " self.embeddings = OllamaEmbeddings(\n", + " base_url=base_url,\n", + " model=model,\n", + " )\n", + "\n", + " def _batch_embed(self, texts: List[str]) -> np.ndarray:\n", + " vectors = []\n", + "\n", + " for i in range(0, len(texts), self.batch_size):\n", + " batch = texts[i : i + self.batch_size]\n", + " batch_vectors = self.embeddings.embed_documents(batch)\n", + " vectors.extend(batch_vectors)\n", + "\n", + " return np.asarray(vectors, dtype=np.float32)\n", + "\n", + " def encode_queries(self, queries: List[str], **kwargs) -> np.ndarray:\n", + " \"\"\"\n", + " BEIR query encoder\n", + " \"\"\"\n", + " return self._batch_embed(queries)\n", + "\n", + " def encode_corpus(\n", + " self,\n", + " corpus: Union[List[Dict[str, str]], Dict[str, Dict[str, str]]],\n", + " **kwargs,\n", + " ) -> np.ndarray:\n", + " \"\"\"\n", + " BEIR corpus encoder\n", + " \"\"\"\n", + " if isinstance(corpus, dict):\n", + " corpus = list(corpus.values())\n", + "\n", + " texts = []\n", + " for doc in corpus:\n", + " title = (doc.get(\"title\") or \"\").strip()\n", + " text = (doc.get(\"text\") or \"\").strip()\n", + "\n", + " if title:\n", + " texts.append(f\"{title}\\n{text}\")\n", + " else:\n", + " texts.append(text)\n", + "\n", + " return self._batch_embed(texts)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "af3eb66d", + "metadata": {}, + "outputs": [], + "source": [ + "def convert_hf_to_beir(hf_dataset):\n", + " corpus, queries, qrels = {}, {}, {}\n", + " \n", + " for i, data in enumerate(hf_dataset):\n", + " docid = f\"doc_{i}\"\n", + " queryid = f\"q_{i}\"\n", + " \n", + " # El código es el documento (lo que el agente debe recuperar)\n", + " corpus[docid] = {\"title\": data.get(\"func_name\", \"\"), \"text\": data['code']}\n", + " \n", + " # El docstring es la consulta (lo que el usuario pide)\n", + " queries[queryid] = data['docstring']\n", + " \n", + " # Relación 1 a 1: la query i busca el código i\n", + " qrels[queryid] = {docid: 1}\n", + " \n", + " return corpus, queries, qrels" + ] + }, + { + "cell_type": "markdown", + "id": "c9528fb6", + "metadata": {}, + "source": [ + "# Data" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "230aae25", + "metadata": {}, + "outputs": [], + "source": [ + "raw_dataset = load_dataset(\"google/code_x_glue_tc_nl_code_search_adv\", split=\"test\")\n", + "corpus, queries, qrels = convert_hf_to_beir(raw_dataset)" + ] + }, + { + "cell_type": "markdown", + "id": "13050d31", + "metadata": {}, + "source": [ + "# Test qwen3-0.6B-emb:latest" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "514540af", + "metadata": {}, + "outputs": [], + "source": [ + "model = BEIROllamaEmbeddings(\n", + " base_url=\"http://localhost:11434\",\n", + " model=\"qwen3-0.6B-emb:latest\",\n", + " batch_size=64,\n", + ")\n", + "\n", + "# Inicializar buscador y evaluador\n", + "retriever = DenseRetrievalExactSearch(model, batch_size=64)\n", + "evaluator = EvaluateRetrieval(retriever, score_function=\"cos_sim\")\n", + "\n", + "# Ejecutar recuperación\n", + "results = evaluator.retrieve(corpus, queries)\n", + "\n", + "# Evaluar métricas (NDCG, MAP, Recall, Precision)\n", + "ndcg, _map, recall, precision = evaluator.evaluate(\n", + " qrels, results, [1, 3, 5, 10]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "5c0f9845", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Resultados para CodeXGLUE:\n", + "NDCG: {'NDCG@1': 0.94971, 'NDCG@3': 0.96956, 'NDCG@5': 0.97166, 'NDCG@10': 0.97342}\n", + "MAP: {'MAP@1': 0.94971, 'MAP@3': 0.96504, 'MAP@5': 0.9662, 'MAP@10': 0.96694}\n", + "Recall: {'Recall@1': 0.94971, 'Recall@3': 0.98251, 'Recall@5': 0.98761, 'Recall@10': 0.99297}\n", + "Precision: {'P@1': 0.94971, 'P@3': 0.3275, 'P@5': 0.19752, 'P@10': 0.0993}\n" + ] + } + ], + "source": [ + "print(f\"Resultados para CodeXGLUE:\")\n", + "print(\"NDCG:\", ndcg)\n", + "print(\"MAP:\", _map)\n", + "print(\"Recall:\", recall)\n", + "print(\"Precision:\", precision)" + ] + }, + { + "cell_type": "markdown", + "id": "c4e643ca", + "metadata": {}, + "source": [ + "# Test qwen2.5:1.5b" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "5ced1c25", + "metadata": {}, + "outputs": [], + "source": [ + "model_q2 = BEIROllamaEmbeddings(\n", + " base_url=\"http://localhost:11434\",\n", + " model=\"qwen2.5:1.5b\",\n", + " batch_size=64,\n", + ")\n", + "\n", + "# Inicializar buscador y evaluador\n", + "retriever_q2 = DenseRetrievalExactSearch(model_q2, batch_size=64)\n", + "evaluator_q2 = EvaluateRetrieval(retriever_q2, score_function=\"cos_sim\")\n", + "\n", + "# Ejecutar recuperación\n", + "results_q2 = evaluator_q2.retrieve(corpus, queries)\n", + "\n", + "# Evaluar métricas (NDCG, MAP, Recall, Precision)\n", + "ndcg_qwen_2, _map_qwen_2, recall_qwen_2, precision_qwen_2 = evaluator_q2.evaluate(\n", + " qrels, results_q2, [1, 3, 5, 10]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "6a95189e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Resultados para CodeXGLUE:\n", + "NDCG: {'NDCG@1': 0.00031, 'NDCG@3': 0.00061, 'NDCG@5': 0.00086, 'NDCG@10': 0.00118}\n", + "MAP: {'MAP@1': 0.00031, 'MAP@3': 0.00051, 'MAP@5': 0.00065, 'MAP@10': 0.00078}\n", + "Recall: {'Recall@1': 0.00031, 'Recall@3': 0.00088, 'Recall@5': 0.00151, 'Recall@10': 0.0025}\n", + "Precision: {'P@1': 0.00031, 'P@3': 0.00029, 'P@5': 0.0003, 'P@10': 0.00025}\n" + ] + } + ], + "source": [ + "print(f\"Resultados para CodeXGLUE:\")\n", + "print(\"NDCG:\", ndcg_qwen_2)\n", + "print(\"MAP:\", _map_qwen_2)\n", + "print(\"Recall:\", recall_qwen_2)\n", + "print(\"Precision:\", precision_qwen_2)" + ] + }, + { + "cell_type": "markdown", + "id": "3dad9811", + "metadata": {}, + "source": [ + "# Save data" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "f875dd8d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Resultados guardados en /home/pseco/VsCodeProjects/assistance-engine/data/interim/beir_CodeXGlue_results.json\n" + ] + } + ], + "source": [ + "results_data = {\n", + " \"qwen3-0.6B-emb:latest\": {\n", + " \"NDCG\": ndcg,\n", + " \"MAP\": _map,\n", + " \"Recall\": recall,\n", + " \"Precision\": precision,\n", + " },\n", + " \"qwen2.5:1.5b\": {\n", + " \"NDCG\": ndcg_qwen_2,\n", + " \"MAP\": _map_qwen_2,\n", + " \"Recall\": recall_qwen_2,\n", + " \"Precision\": precision_qwen_2,\n", + " }\n", + "}\n", + "\n", + "output_file = \"/home/pseco/VsCodeProjects/assistance-engine/data/interim/beir_CodeXGlue_results.json\"\n", + "with open(output_file, \"w\") as f:\n", + " json.dump(results_data, f, indent=2)\n", + "\n", + "print(f\"Resultados guardados en {output_file}\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "assistance-engine", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/research/embeddings/n00 Beir Analysis.ipynb b/research/embeddings/n00 Beir Analysis.ipynb new file mode 100644 index 0000000..fcd0587 --- /dev/null +++ b/research/embeddings/n00 Beir Analysis.ipynb @@ -0,0 +1,323 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "66cbbaf8", + "metadata": {}, + "source": [ + "# Libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "c01c19dc", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Dict, List, Union\n", + "import numpy as np\n", + "from langchain_ollama import OllamaEmbeddings\n", + "from beir.datasets.data_loader import GenericDataLoader\n", + "from beir.retrieval.search.dense import DenseRetrievalExactSearch\n", + "from beir.retrieval.evaluation import EvaluateRetrieval\n", + "from beir import util\n", + "import json" + ] + }, + { + "cell_type": "markdown", + "id": "ac011c1c", + "metadata": {}, + "source": [ + "# Utils" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "b83e7900", + "metadata": {}, + "outputs": [], + "source": [ + "class BEIROllamaEmbeddings:\n", + " \"\"\"\n", + " Adapter that makes LangChain's OllamaEmbeddings compatible with BEIR.\n", + " \"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " base_url: str,\n", + " model: str,\n", + " batch_size: int = 64,\n", + " ) -> None:\n", + " self.batch_size = batch_size\n", + " self.embeddings = OllamaEmbeddings(\n", + " base_url=base_url,\n", + " model=model,\n", + " )\n", + "\n", + " def _batch_embed(self, texts: List[str]) -> np.ndarray:\n", + " vectors = []\n", + "\n", + " for i in range(0, len(texts), self.batch_size):\n", + " batch = texts[i : i + self.batch_size]\n", + " batch_vectors = self.embeddings.embed_documents(batch)\n", + " vectors.extend(batch_vectors)\n", + "\n", + " return np.asarray(vectors, dtype=np.float32)\n", + "\n", + " def encode_queries(self, queries: List[str], **kwargs) -> np.ndarray:\n", + " \"\"\"\n", + " BEIR query encoder\n", + " \"\"\"\n", + " return self._batch_embed(queries)\n", + "\n", + " def encode_corpus(\n", + " self,\n", + " corpus: Union[List[Dict[str, str]], Dict[str, Dict[str, str]]],\n", + " **kwargs,\n", + " ) -> np.ndarray:\n", + " \"\"\"\n", + " BEIR corpus encoder\n", + " \"\"\"\n", + " if isinstance(corpus, dict):\n", + " corpus = list(corpus.values())\n", + "\n", + " texts = []\n", + " for doc in corpus:\n", + " title = (doc.get(\"title\") or \"\").strip()\n", + " text = (doc.get(\"text\") or \"\").strip()\n", + "\n", + " if title:\n", + " texts.append(f\"{title}\\n{text}\")\n", + " else:\n", + " texts.append(text)\n", + "\n", + " return self._batch_embed(texts)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af3eb66d", + "metadata": {}, + "outputs": [], + "source": [ + "def convert_codexglue_to_beir(input_file):\n", + " corpus, queries, qrels = {}, {}, {}\n", + " with open(input_file, 'r') as f:\n", + " for i, line in enumerate(f):\n", + " data = json.loads(line)\n", + " docid = f\"doc_{i}\"\n", + " queryid = f\"q_{i}\"\n", + " \n", + " # El código es nuestro documento (Corpus)\n", + " corpus[docid] = {\"title\": \"\", \"text\": data['code']}\n", + " # El docstring es nuestra consulta (Query)\n", + " queries[queryid] = data['docstring']\n", + " # En CodeXGLUE, la consulta i corresponde al código i\n", + " qrels[queryid] = {docid: 1}\n", + " \n", + " return corpus, queries, qrels\n", + "\n", + "# Carga tus datos (ejemplo con el set de test de AdvTest)\n", + "corpus, queries, qrels = convert_codexglue_to_beir(\"test.jsonl\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "c9528fb6", + "metadata": {}, + "source": [ + "# Data" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "230aae25", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "1915c67ec20f4806b30b48eff9a132e2", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/5183 [00:00 None:\n", + " self.batch_size = batch_size\n", + " self.embeddings = OllamaEmbeddings(\n", + " base_url=base_url,\n", + " model=model,\n", + " )\n", + "\n", + " def _batch_embed(self, texts: List[str]) -> np.ndarray:\n", + " vectors = []\n", + "\n", + " for i in range(0, len(texts), self.batch_size):\n", + " batch = texts[i : i + self.batch_size]\n", + " batch_vectors = self.embeddings.embed_documents(batch)\n", + " vectors.extend(batch_vectors)\n", + "\n", + " return np.asarray(vectors, dtype=np.float32)\n", + "\n", + " def encode_queries(self, queries: List[str], **kwargs) -> np.ndarray:\n", + " \"\"\"\n", + " BEIR query encoder\n", + " \"\"\"\n", + " return self._batch_embed(queries)\n", + "\n", + " def encode_corpus(\n", + " self,\n", + " corpus: Union[List[Dict[str, str]], Dict[str, Dict[str, str]]],\n", + " **kwargs,\n", + " ) -> np.ndarray:\n", + " \"\"\"\n", + " BEIR corpus encoder\n", + " \"\"\"\n", + " if isinstance(corpus, dict):\n", + " corpus = list(corpus.values())\n", + "\n", + " texts = []\n", + " for doc in corpus:\n", + " title = (doc.get(\"title\") or \"\").strip()\n", + " text = (doc.get(\"text\") or \"\").strip()\n", + "\n", + " if title:\n", + " texts.append(f\"{title}\\n{text}\")\n", + " else:\n", + " texts.append(text)\n", + "\n", + " return self._batch_embed(texts)" + ] + }, + { + "cell_type": "markdown", + "id": "c9528fb6", + "metadata": {}, + "source": [ + "# Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "230aae25", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Descargando datos de Hugging Face...\n", + "Cargando con BEIR GenericDataLoader...\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "0e67479e959248f598db3415efbb13ae", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/20604 [00:00>>>>>> 4b5352d93cf89b7562895b550fb5bd62160586c5 + } + ], + "metadata": { + "kernelspec": { + "display_name": "assistance-engine", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/research/embeddings/n00 first Analysis.ipynb b/research/embeddings/n00 first Analysis.ipynb new file mode 100644 index 0000000..fa0f304 --- /dev/null +++ b/research/embeddings/n00 first Analysis.ipynb @@ -0,0 +1,289 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "096e6224", + "metadata": {}, + "source": [ + "# Libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "4b0853e9", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_2931729/1845255288.py:4: DeprecationWarning: Importing SemanticSimilarity from 'ragas.metrics' is deprecated and will be removed in v1.0. Please use 'ragas.metrics.collections' instead. Example: from ragas.metrics.collections import SemanticSimilarity\n", + " from ragas.metrics import SemanticSimilarity\n" + ] + } + ], + "source": [ + "# ...existing code...\n", + "from datasets import load_dataset\n", + "from ragas import EvaluationDataset, evaluate\n", + "from ragas.metrics import SemanticSimilarity\n", + "from langchain_community.embeddings import OllamaEmbeddings\n", + "import asyncio\n", + "from typing import Sequence\n", + "from ragas.embeddings.base import BaseRagasEmbedding\n", + "import os\n", + "from transformers import AutoConfig\n", + "import nltk\n", + "# ...existing code..." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "6bfe1ca0", + "metadata": {}, + "outputs": [], + "source": [ + "nltk.download(\"punkt\", quiet=True)\n", + "\n", + "ES_URL = os.getenv(\"ELASTICSEARCH_LOCAL_URL\")\n", + "ES_INDEX_NAME = os.getenv(\"ELASTICSEARCH_INDEX\")\n", + "HF_EMBEDDING_MODEL_NAME = os.getenv(\"HF_EMBEDDING_MODEL_NAME\")\n", + "BASE_URL = os.getenv(\"LLM_BASE_LOCAL_URL\")\n", + "MODEL_NAME = os.getenv(\"OLLAMA_MODEL_NAME\")\n", + "\n", + "config = AutoConfig.from_pretrained(HF_EMBEDDING_MODEL_NAME)\n", + "embedding_dim = config.hidden_size" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "ea41ce0f", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_2931729/256987240.py:1: LangChainDeprecationWarning: The class `OllamaEmbeddings` was deprecated in LangChain 0.3.1 and will be removed in 1.0.0. An updated version of the class exists in the `langchain-ollama package and should be used instead. To use it run `pip install -U `langchain-ollama` and import as `from `langchain_ollama import OllamaEmbeddings``.\n", + " embeddings = OllamaEmbeddings(base_url=BASE_URL, model=MODEL_NAME)\n" + ] + } + ], + "source": [ + "embeddings = OllamaEmbeddings(base_url=BASE_URL, model=MODEL_NAME)" + ] + }, + { + "cell_type": "markdown", + "id": "8eee9390", + "metadata": {}, + "source": [ + "# Similitud Aleatoria" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d7b150e5", + "metadata": {}, + "outputs": [], + "source": [ + "from datasets import load_dataset\n", + "from ragas import EvaluationDataset\n", + "\n", + "\n", + "def _normalize_answer(answer_value: object) -> str:\n", + " \"\"\"\n", + " Normalize answer values to a single string.\n", + " \"\"\"\n", + " if isinstance(answer_value, dict):\n", + " text_value = answer_value.get(\"text\")\n", + " if isinstance(text_value, list):\n", + " return str(text_value[0]) if text_value else \"\"\n", + " if text_value is not None:\n", + " return str(text_value)\n", + "\n", + " if isinstance(answer_value, list):\n", + " return str(answer_value[0]) if answer_value else \"\"\n", + "\n", + " return str(answer_value)\n", + "\n", + "\n", + "def _first_existing_key(candidates: list[str], keys: set[str]) -> str | None:\n", + " \"\"\"\n", + " Return the first key present in keys from candidates.\n", + " \"\"\"\n", + " for candidate in candidates:\n", + " if candidate in keys:\n", + " return candidate\n", + " return None\n", + "\n", + "\n", + "ds = load_dataset(\"sentence-transformers/natural-questions\")\n", + "train_ds = ds[\"train\"]\n", + "\n", + "max_questions = min(100, len(train_ds))\n", + "train_ds = train_ds.select(range(max_questions))\n", + "\n", + "available_keys = set(train_ds.column_names)\n", + "reference_key = _first_existing_key(\n", + " [\"question\", \"query\", \"text\", \"input\"], available_keys\n", + ")\n", + "response_key = _first_existing_key(\n", + " [\"answer\", \"answers\", \"response\", \"output\"], available_keys\n", + ")\n", + "\n", + "if reference_key is None or response_key is None:\n", + " raise KeyError(\n", + " f\"Expected question/answer-like columns not found. \"\n", + " f\"Available columns: {train_ds.column_names}\"\n", + " )\n", + "\n", + "rows = []\n", + "for row in train_ds:\n", + " rows.append(\n", + " {\n", + " \"reference\": str(row[reference_key]),\n", + " \"response\": _normalize_answer(row[response_key]),\n", + " }\n", + " )\n", + "\n", + "eval_ds = EvaluationDataset.from_list(rows)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "753aab30", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DatasetDict({\n", + " train: Dataset({\n", + " features: ['query', 'answer'],\n", + " num_rows: 100231\n", + " })\n", + "})\n", + "['query', 'answer']\n", + "{'query': 'when did richmond last play in a preliminary final', 'answer': \"Richmond Football Club Richmond began 2017 with 5 straight wins, a feat it had not achieved since 1995. A series of close losses hampered the Tigers throughout the middle of the season, including a 5-point loss to the Western Bulldogs, 2-point loss to Fremantle, and a 3-point loss to the Giants. Richmond ended the season strongly with convincing victories over Fremantle and St Kilda in the final two rounds, elevating the club to 3rd on the ladder. Richmond's first final of the season against the Cats at the MCG attracted a record qualifying final crowd of 95,028; the Tigers won by 51 points. Having advanced to the first preliminary finals for the first time since 2001, Richmond defeated Greater Western Sydney by 36 points in front of a crowd of 94,258 to progress to the Grand Final against Adelaide, their first Grand Final appearance since 1982. The attendance was 100,021, the largest crowd to a grand final since 1986. The Crows led at quarter time and led by as many as 13, but the Tigers took over the game as it progressed and scored seven straight goals at one point. They eventually would win by 48 points – 16.12 (108) to Adelaide's 8.12 (60) – to end their 37-year flag drought.[22] Dustin Martin also became the first player to win a Premiership medal, the Brownlow Medal and the Norm Smith Medal in the same season, while Damien Hardwick was named AFL Coaches Association Coach of the Year. Richmond's jump from 13th to premiers also marked the biggest jump from one AFL season to the next.\"}\n" + ] + } + ], + "source": [ + "print(ds)\n", + "print(ds[\"train\"].column_names)\n", + "print(ds[\"train\"][0])" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "6c3d4235", + "metadata": {}, + "outputs": [], + "source": [ + "# ...existing code...\n", + "class OllamaRagasEmbeddingAdapter(BaseRagasEmbedding):\n", + " \"\"\"Adaptador de LangChain Ollama a la API moderna de embeddings en Ragas.\"\"\"\n", + "\n", + " def __init__(self, base_url: str, model_name: str) -> None:\n", + " self._client = OllamaEmbeddings(base_url=base_url, model=model_name)\n", + "\n", + " def embed_text(self, text: str) -> list[float]:\n", + " return self._client.embed_query(text)\n", + "\n", + " async def aembed_text(self, text: str) -> list[float]:\n", + " return await asyncio.to_thread(self.embed_text, text)\n", + "\n", + " def embed_query(self, text: str) -> list[float]:\n", + " return self.embed_text(text)\n", + "\n", + " def embed_documents(self, texts: Sequence[str]) -> list[list[float]]:\n", + " return self._client.embed_documents(list(texts))\n", + "\n", + " async def aembed_query(self, text: str) -> list[float]:\n", + " return await self.aembed_text(text)\n", + "\n", + " async def aembed_documents(\n", + " self, texts: Sequence[str]\n", + " ) -> list[list[float]]:\n", + " return await asyncio.to_thread(self.embed_documents, texts)\n", + "\n", + "\n", + "if not BASE_URL or not MODEL_NAME:\n", + " raise ValueError(\n", + " \"Faltan variables de entorno: LLM_BASE_LOCAL_URL u OLLAMA_MODEL_NAME.\"\n", + " )\n", + "\n", + "embeddings = OllamaRagasEmbeddingAdapter(\n", + " base_url=BASE_URL,\n", + " model_name=MODEL_NAME,\n", + ")\n", + "\n", + "semantic_sim = SemanticSimilarity()\n", + "# ...existing code..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54aacf01", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "6a4b6e91c71d4849922f36d45f3e9f7f", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Evaluating: 0%| | 0/100231 [00:00