assistance-engine/scratches/acano/langgraph_agent_simple.ipynb

836 lines
58 KiB
Plaintext

{
"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": [
"<IPython.core.display.Image object>"
]
},
"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": "iVBORw0KGgoAAAANSUhEUgAAANgAAAD5CAIAAADKsmwpAAAQAElEQVR4nOydCXwTRfvHZzdJk170vuhBWwpFzooFFBUQEPXlKCiKXAK+nAriX8DjBQTxVRBFQeUUEMpV5aaAHHJLuXk5ClKEllJ6l57plWP3/2y2TdM2KRTY7WwyX2g+uzOTTbL55ZmZZ2aekbMsiwiEhkaOCAQMIEIkYAERIgELiBAJWECESMACIkQCFhAh1iQ7RXv1VH5ehkajYfRaRq+pWYCiEOfxMvV6USxiKVqGGH2twjTLZTMVpyxl+IdYWkaxZgojY8mKY8rwcky1YrQcMbpqKUpHWianVY60X6hDZA8XJEEo4kfkSb2pObwlQ52n1elYuZxSOsjsVDRoS1fO1CzKSYOtnUDLKUZX62bSoKUqJdE0xTAsS3EHrL5mYUpWlcgLER4NOq5WUqag9NpqKSoHuU7Pakr05aUMHNgpab8Q+z6jfZF0IEJEmcmaXb+k6soYZ09FxPNurV90RpKGRUe35Ny+qi4r1fsEqgZ+4I+kgK0L8bfvU7NTS4PCnfqNlZL9eBjup2t3r0otKdR3H+gb3tER4Y1NC3HlzCQZTY36IhhZL9fiik7szA5oBjW1H8IY2xXiyhmJgc2cXhnhjWyAlTOSOvRyb9cF336MjQpx+WeJTds69xzshWyGX2bc8Q5QRo3H1C7SyPZYPetOYHMHm1IhMOa/wVl3S09sy0FYYnNC3LU8Hbwt/xplbV2Th2HMl6GX/8pHWGJjQtSjlJvFo2YHI9tEhoKaO/46+w7CD9sS4rp5KV4B9siG6Tfer1Stv3lRjTDDtoRYmFv+1ofScPAKh1+I6sSObIQZNiTE2BXp9g5ybsRNRD799NOdO3ei+vPyyy+npqYiAYga519WzCDMsCEhZtwpC2rpgMTl+vXrqP6kp6fn5eUhYaDlyE5JHdqEl1G0ISFqypnI7h5IGE6ePDlu3LgXXnihf//+s2bNysnhvCSRkZFpaWlffvllt27d4FStVi9btmzEiBF8sR9++KGsrIx/eo8ePTZt2jRmzBh4yrFjx/r27QuJUVFRU6ZMQQLg6q1MSyxFOGErQrx9pYSmkauPDAnAjRs3Jk+e3KFDhy1btnz88cc3b96cPXs2MqgTHmfOnHn06FE4iImJWbNmzfDhwxcuXAjlDx48uGLFCv4KCoVi+/bt4eHhixcvfv7556EAJEKdvmDBAiQAfiEO5WV6hBO2Mh8x406pXCHUr+7SpUsqlerdd9+ladrX17dly5a3bt2qXWzYsGFg+UJCQvjTy5cvx8XFffDBB4ibSEa5uLhMnToViYKXvyL+JF7NRFsRYkmRXjjrHxERAZXshx9+2KlTpy5dugQGBkINW7sYmL1Tp05BxQ0mU6fjpra6u7sbc0G+SCzcvewYBq+hXVupmrn7LtioeosWLX788UcvL6+ffvppwIAB7733Hli72sUgF+piKLBjx47z58+PGjXKNNfOzg6JhlyGRHYfPAhbEaLKScYIWRd17twZ2oKxsbHQOiwoKADryNs8IyzLbt26ddCgQSBEqL4hpaioCDUQBVl49VSQ7QjR11/F6IWyiBcuXIDWHhyAUezTpw90dUFk4IIxLaPVaktLS729K2adaTSa48ePowYi466GlhOL2BCEd3TS69jyEkG0CBUxdJa3bdsGzr/4+HjoHYMi/fz8lEolKO/06dNQEUM/Jjg4eNeuXffu3cvPz58zZw60LAsLC4uLi2tfEErCI3Sr4WpIADKSSu1UeH31NuRHpGnq1F5BJkFBdxgq3O+++w6GQ8aOHevo6AhtQbmc6whCV/rcuXNgI8Ecfv3119C5HjhwIDgRO3bsOHHiRDjt2bMn+BprXDAgIABcieB0hGYlEoD7GeW+ASqEEzY0MXbzwnslhboRnwcjm+en//tn9JxQe2dBvKqPhg1ZxJ5v+xTl6ZDNsz86095JjpUKkU0tsHfzVSgd6J1L06ImNDZbQK/Xg8PZbBb0LcALCG7n2lmhoaGrV69GwrDGgNksJycnGDM0m9WqVSsYoUEWuHWlqH13d4QZtrVm5d6tsh1L7k38PsxSgdrNNR74yuGLN5sFbUFjX/iJU2TAbBa40KGJaTYLfjPQWzKbtX9dVlJ80fhvmiLMsLnFUxvm3QU/zvDpTZBNsmTqrQETmvg1VSDMsLk1K0M/DYLhvrP7hJpkhTOrZ93xb+qAoQqRba7iGzcv9Pyh3KIs26oKNs6/Z6eUWWofNzi2u8B+ybTbPd/ybd4B91gcT4ToL++6N7br82981y7adMiRJVNu+wXbD5iEqZF4UqyamQT+miGfBCKMsfUgTKs+T9Jp2E6vekR0k2RYwbrZ/nNa2p3SZu2cew3HPbIKCUuH4mJzL5/Io+V0YJj9a+/4UtJ3rSZeLjl78H5uhsaxkXwE+Afwcl2bhwixguNbcxIuFpaXMuC0hlEHJxc7p0YKWq7XaqruD01zf4yOqTzlom7K5JTeEJ/TNH6nXEHpKmNp8sW4AgpE6RE/G81YmAsdCzAV12cqD1hDeE9jiiHcJ1xWptPqjSWNEWbB167TUaVqnbpAX6bm3o2Lh6LrG94BzfAaUK4DIsSanNiRk3qrtEyt1+lY+LL1JkFguYEVuGFMxfgKrwOjVqoLEem0yLQY4tQDN5vS60HrFEXzAZANsY1Zin+i8Qr8CA4c1whOK1MgvbaqpDEXhEjLKaW9zNldHv60c3gHJyQ1iBDFZtKkSUOGDHnuuecQwQQSzF1sdDodP0OMYAq5I2JDhGgWckfEhgjRLOSOiI1Wq1UocBztbViIEMWGWESzkDsiNkSIZiF3RGyIEM1C7ojYgBBJG7E2RIhiQyyiWcgdERsiRLOQOyI2RIhmIXdEbIgQzULuiNiAQ5sIsTbkjogKN/OQYWQyKUxVFRciRFEh9bIlyE0RFSJES5CbIipkxoMliBBFhVhES5CbIipEiJYgN0VUiBAtQW6KqBAhWoLcFFEhnRVLECGKCrGIliA3RWwsxXK1cYgQRQUG9zIyMhChFkSIogL1co2t0Qg8RIiiQoRoCSJEUSFCtAQRoqgQIVqCCFFUiBAtQYQoKkSIliBCFBUiREsQIYoKEaIliBBFBYSo1+sRoRa2uPNUwwKDK0SLtSFCFBtSO5uFCFFsiBDNQtqIYkOEaBYiRLEhQjQLEaLYECGahQhRbIgQzUJ2nhKJiIgImq7oGsI9pw37ofXp02fOnDmIQHrNotG2bVvEbcfHAa5EiqL8/PyGDRuGCAaIEEXinXfecXR0NE1p165d8+bNEcEAEaJI9OzZ01R2Hh4egwcPRoRKiBDFY+TIkY0aNeKPW7Ro0aZNG0SohAhRPF588cXw8HA4cHFxGTp0KCKYQHrNtdCj47vyigs1Oo2eklGsnrs/tJzb/JtlKZpi+a3jK+F2locsKMltKc4gmYwrxm1ZTxn29TbcXZlhm3rIzc/Pj7921cnRKSLiae4iFHwBlXvU04Ydxhl+r3ruJeCgIst4ivg/wzXllOmm5oCdvdw30L5dV2ckQYgQq7F5QWp2RplCKWMZVq9luQqD33xehkBa8GdQInfbKE54iHtgub3oWYqlKcqgSE5HfBlOaPyO9DJQMc3vYw+CNGxfT3HKQobr8Ok0C2IzPJFXYlWW4RKGbe3Zqg3tKRnL6inTN2+nAmly2u8xyDfsaQckKYhDu4qdy9OKC5nhM5oiKXP7kvrPmEzazie0lZS0SCxiBdsWpZWo9VETA5FVsP6rxGHTQp2lE92EdFYqyLhX1mNoALIWPH1VsatSkHQgQuSIP1EkkyMnNwpZC36hDsWFUhrRJm1EDqiUGS2yJlSOlFYjpQUJRIgcOkanZ6yqrcyyVa4fSUCESMACIkTrRHK+ECJEDor3LlsRlNQ+DxEiB9gPK/SmslISIxEiDz+sZl1QUvpERIgGqIo/q4GVlAoREWIFVjfOSUmqXkZEiBVYWVdFghAhGrDKiR+S+nURIXJQlOTcHQ+Am1RFRlYkh8F9Y1VWkZKaa5QIkYcl7cSGhUwDM4B33bx9x+9zv5mFrBpiEQ2wWE9UT0i4jqwdIsRHRK1Wb96y/uy5U3fu3PZw9+zcueu7oyaoVCrELb1jFv34zV8nj9op7Hr0eLV1q3afTf9w6+b97u4eOp1u1eolp8/8lZWV0bp1xICot5599gX+gv1f7zlq5PiCgvy10Svs7e07RD438f2pHh6eH3409vLli1DgwIE9sTuPOjk5PczbY6U23EyqZo5HqJm3bY/ZuGnNoLeGf/3VwnHjJh89dhAExGdt3rIhdve2SROnLVu23t7eAZSHDFFv4PHHn+Zv2bpxQP9BGzfEdu3SY9YXHx87foh/lkKh+O23aCi2Y/uhtb9uvRp/ac3a5ZC+8PsVTz3Vulev3kcOnX9IFaKKVa5IQhCLaKD+fZW33hwGSmrSJIQ/jY+/fPZc3LixH8Dx/gO7u7zYvVvXnnA8dMgoSOfLlJeXQ9aQwSP79X0DTv/1WhQ8K3rdL3AdvoC/f+Cwoe9yR07OYBFv3vwb2QxEiDz1biOCATt3/tS8b2bdun2Tj3fo5uYOj3q9/s6dxNde7Wcs2eXFHleu/A8OQFgajQYUZsyKaPfMH/t2FRQWuDRygdPmzZ8yZjk7NyouViObgQiR4xEqsRW//LR37w6olEFYPj6+K1ct3vvHTkhXF6tB1A4OVYG/XFxc+QO1uggeJ03+d41L5eXe54X4hLvuxI9o9YDUYndvHfjGkD69B/ApvMgAB3tuWbtWW7UWKy/vPn/g4cktM57y0XSogk2v5u3tiwR5l0hCECFyUBRdL2ME9W9paamnpzd/ChVu3Knj/DFU2d7ePtCVNhY+GXeMPwjwD1IqlXDwdEQkn5KXl2swnxILDyIEpNfMwbJMvRqJcrk8KCgYmnepaffA4TL/uzltWkcUFRUWFxdDbufnuhw4uOfc+dNwTehBQzr/LBDcyBHjoHdy9eol0C70l6d+/N7CRfMe+HJgQf/+O/7i/86ZGlorgwiRg6r/0OzM6V+rlKqRowYOe6f/M+07jh49EU4HvNEzPSNtxDtj27R5+uNPJg5/Z0BychLU4IjTrgIe3x70zrSpn2+MWdM3qhv4Ghv7BUyZMuOBr9W39+vwBqd9/H5JSTGyUkjsG464PTkXDxWMmPVkwi+VlZWBvxpMJn8a81v0hg2rY3cdRSJy40zBmX3ZE78PQxKBWESOJ9tdBeWNHT9067YYqLUPHznw++b1/foNROLCQFeF9JqlB/skF3mMHDG2oCDvwIHdv6z8ycvLB8ZRwK2NxIWuDM0oFYgQObgW4hNd5DH5g08QoT4QIXIwpKHc0BAhGrC6SA+SgwiRg7JGJUrrIxEhWiustNbYEyFysHjP0H4kSK9ZgtR3rJnwxCFCNMDt2WNdEWOlFlWKCJGDAS+i1ILF1A0ltfWxRIgctAQjW1oZRIgcLGt98cAkBhEih52dXKGyLpNII4VChqQDmX3DEdDUgZHSvjamgAAAEABJREFU7jgPJj9dK62fFhEih2+onZ0dfe6PXGQt3LutbhwqpRUIRIgVvDqiccLFPGQV7FudzjLsqyO8kXQgM7QrKC0t/Wjy9DYu73v4qoJbNFI6srrq8QWNjjlTD10Nb50l5131p7A15qwadg9n635WjXRkLktOy+6na1ISCpWOssHTJLbBJRFiBevWrWvVqlX71u1jFqUU5eo0OobRmb8zho3pzV/ErFiNp5WJrDF4PFvrgtUkW5le4xUtCVShpBQKuVaW2eZlbbNmzby9iUWUDrm5uYsWLfriiy+QWEyePHnQoEGdO3dGArBq1aoVK7gYTs7Ozo0aNQoKCmrXrl3z5s3bt2+P8MbW3TczZswAZSAR8fT0dHR0RMIwdOjQPXv23L17V61Wp6am3rhx4+DBg66urvCKO3fuRBhjoxYxIyPjzJkzUVFRyOpYtmzZypUrayTCt3zhwgWEMbbYay4oKBg9evSzzz6LGgL4DZSXlyPBGDhwoL+/v2mKUqnEXIXI1oSYnp4OFZZOp9u9e7ePjw9qCD755JNbt24hwYCq/4UXXjBWdHAwd+5chD02JMTLly+PHTsWvicPDw/UcMAPQOhgN4MHD/by4gI+8TXyjh07li5divDGJoSYmZmJDHEyY2Nj+TBIDcj8+fNDQkKQkAQEBERGRjIM4+vLxRn7/vvvYeBo0qRJCGOsv7MCvcXDhw+DjwbhAbQNwCjK5YL7K3r16nXgwAHj6alTp6ZPnx4dHQ0yRfhhzRaxsJALw1VSUoKPCoEJEyZkZWUh4TFVIfDcc89BHT1x4sT9+/cj/LBaIa5evXrv3r3I0GBCOAHVJTicUUMALm7Q4vHjx3/44QeEGVZYNWu12uzsbLjj7733HiKYY+PGjdBcqe1ubECsTYhwc6FtBFYHmucIS2DYA1pp/G4XDQj4EMaPH7927VoYAEQYYFVV85YtW8BHCAOs2KoQGDZsWFlZGWpoYAwa6ujZs2dD1YEwwEqEuHnzZnjs3r07/MoR3jRu3BiT34lCoYA6Oj4+/quvvkINjTUIccqUKXwDw93dHWFPTEyMCL6bh2fGjBktW7YcOnQov1tMQyHtNuL58+fBcwueuRqjqziTnJzcpEkThBkJCQkjRoxYvnw5VNmoIZCqRdRoNDC6zzf5JaRCaB2C7UH4ER4efvr06R9//HHTpk2oIZCkEHNzc3NychYsWID/fM8aQP0TGhqKcGXVqlVpaWlQWSPRkVjVDPobM2YMOKvd3NwQQRj27du3YsUK8Ow4OzsjsZCYELdt29ahQ4fAwEAkTfR6fXp6Op6jvaaAsxOajPPmzevUqRMSBWlUzYmJie+//z4cvP7669JVIQBDPvg7mADwxR45ciQ6OhoqHyQK0hAijJd8/vnnSPpQFIVhl9kSixcvLi8vB+8YEh6sq+Zr165duXIFt1kLtsaxY8fmzp0L1lHQ9an4WkToGn/77bd9+vRBVgR4naBbiiRF165d169fP3LkyKtXryLBwFeIMPywZs0aMTtuIlBaWjpr1izJDSJ4enru3bsXvIz8XHchwFSIGzZsOHv2LLI6XFxclixZEhsbyzAMkhqXLl0SbsUZpgvss7KyKCuN4apQKPr165eSkgLDQhIaE/rnn3/CwgTc6xRTIUIHBauZAU8ccEJFRUVt3LhRuKgPTxYQYrNmzZBgYFo1+/r6QrsEWTU7d+5MSEhQq9VICty+fVtQi4ipELdv375r1y5k7cBYeWpqalxcHMIeoatmTIUIY8owFIZsgPDw8JiYGPzt4q1btwQVIqYObRgKg35lQ0UFER9wLsLnxXYMuqCgAAZXDx06hAQDU4vo5eVlOypEhvUDeXl5DTUX8IEIbQ4RtkLcv3//b7/9hmyJNm3agF0EjzfCD9sV4v379yU3FPb48ItvLl68iDBDaN8NwlaIr7zyyttvv41sDwcHB5VK9fXXXyOcAIsotBAxdRo3bOS4hqVly5Y3btxAOGG7VfOxY8fWrl2LbBXoosIjJp5UGI2EvqPQ4fwwFSL4C+7evYtsG+i+TJ06FTU0IjQQEbZVc5cuXSS3Qu+JExISMnLkSNTQiFAvI2wtoqurK/4rjESgdevW8NiwUeRsWohnz57FP+yzaIBdbMAlV+JUzZgKEcZek5KSEMGAm5vbt99+CwfG8DSvvvpq3759kfCUl5dnZWWJsHISUyFGRkby60cJPPySCfB4FxcX9+nTJycnB4YERQhCLIIHkQdTITZq1EhCyy5FY9GiRa+99lpGRgYyLH8RdBYCj9Czv4xgKsRr164tWLAAEaozaNCgkpIS/piiqISEBF6UwiFOTwVhK0S43YJuzyRFhgwZcvv2bdOUzMxM8PwjIRGnp4KwFSIMc02bNg0RTOAnLMpkMmOKRqM5ePAgEhKhVwgYwdSh7ejoiHP4tgYhJibm4sWL586dO3PmDHgV0tPTfRzbs4XuB7fd9PP3RSbLU8G6cGeUYYtywzblLMttN15zy/PqO5BX7GcOBxT3LIpGhQVFwe5dUq5TKWxhRV6tTcu5azKVz6x67cozmvIOUHr6PzhUM14ztEePHg23GN4SVM2FhYXgtgAzAMd//vknIpjw65zEkgI9aEXP+XMoqlJq/HdZdQqCYjmNGHVSpbZKUfGrdrnylc9CleksL2SWoqo/EZkIkqY5IRo1BMpjmCpFyRUgMEphR7V93q3Tv1zr+ER4WUSokdevX2/c+gFcFcgwWxsRTFj+WaJ3kP3ACX4I370TqnEtruDqyVy/YGVQS4s7HeHVRhw2bFjtkb2OHTsiQiUr/pPYMtKj5xDJqBBo1dll0LSQPWvTzx8osFQGLyF6e3v37t3bNMXDwwPPoNMNwh9rs+R2soieLkiCtOzkeunYfUu52PWaBw8ebGoUIyIiMNkaCQcy75Z5+qqQNGnfw12rZTUW1s1iJ0QYU4FRVD7eiLu7+/DhwxGhEm25Tq6S8NY4DINyMs2vDsPxUxmNYmsDiFCJTsPqNFokWRg9y1jYVeixes3aUnRyT3ZOiqYwX6MpYynouutZWgavV+Wyksk5FwNl6OQDFQeU4UDPPUJnn/daGRwElGELCLZbk7n6AL1cJlv6cSJcFp7IVjoF4JRzObH8McsyBq8ChbgLs5VuCt5pVvkUMK80OILtkL2jrEm4w7O9JbBBla3xiELcH52V/LdaW87QclqukFMKudKZqnBb0TTLMEYh8o4lyuBchT/wzPCRAWmKYliDh8rgy+QLVLm7eJ1RFf4thCqejlCVphEvSoPaeF+Z0SVq6vHiPqRcBq+gK9flZWlz0nLP/ZmrtKeh7fxCFFGkqFRzaVan3kL849fMpGtq0J+zp5N/K0mutdNrmJT47Csn8q78lfdMd/dOr0lmyxaKQtIOGskZK/OtwfoJcfknSVD7BbXxc/IWdk2XoMjs6OD2XDyTrMTCC4fzrp8pHDVbGlPOKpskUoWr3yyEyn3Yzsq9hNKfP7rl7O3YomuQpFVoindoo5bdm1Ay+ZKptxGhQXkoIeZnaXcsT235Ukjjlla47j040te3udfiKRLQIgwq07SUK2djk78WDxbi7SulG+entH45hLbeUMLugY6hHQIXT8F9BiT06kynFEgOiqo1e6eSBwtx35q0Zp2sf2WnvYvMM9h9+WdkxVbD8AAhrpie5OzjqHCSIRvAJ8yFklEbvklBBGEw+uBqU5cQD2/OBk9hUFsbmoXV/PnAvMzy9CQNwhLOfWOdm37UKcS/Txd4h9qcy9fRTbV71T2EJZz7RtL+G8tYFOJfO7kZO14hjRCWXLr659SZndTFeehJExLppyllC+/juDMUjEuJ32vu/3rP6HUr0ROCtaA4i0K8fqbA3kWqM44eE4VK/ucmYZdpPhqsyZj7Q/LFnE/3/rETYQNl4QduUYiaMsavmZVvuWMJB3f7jGQcY1mbrg55SBISriOMsPj2zfsGb5wthkaxvasCCcOdu1cOHFmZcu+6k6PbU+Ev9HpptErF7QR28vTmg8dWT3h3aXTMZ5lZiX4+YV06D+7QvmKn3N37fjp/ea/SzuHptq94ewYhwfALc827V4ikz0s9IuHx2+++XLrsh9idR+H45Mlja6NXJN9NcnFxDQsLnzzpEx8fX75wHVk84MXcum3T/v27U+4lNwkKiYx89t1RE0yXtz4EFtsV5i1i0nU1LRfKZZNzP2X5mklabfnEsStHDPkmPfOfpasn6A3L0WRyRWlp0Y49373V/z/fzjndtnX333f8Ny+fqyXjzm6NO7vl9d7TJo/71cOt8cEjq5BgyOxktIxKOFeEMIOi6zfpYd/ek/A4bepMXoXnL5z5fPa0Xr16/x6zd9bMeZmZ6Qt/nMeXrCPLyLZtMes3rB74xpCYjbv79n1jz94dMb9Fo/pQx+wb80IsytXK5EI1ii9e3ieXKUYO/sbHK9jXO/TNqOmp6Qnxf1dELNDrtS+/NLpJYBvwwkdG9IZfYWr6TUj/69TvbVv1AGk6ODQCGxkWGomEBISYlYqdE4dbcPwYX8vqX5d2ebE7KAlsXqtWbd+b8NHp03/dMNTddWQZuXzlYnh4y1de6ePq6tan94DFP6/p1PF5VE/YevkRdTqGooSavA31cmBAS0fHilWu7m5+Hu4BScmXjAWC/FvxBw72XJ+9tKwI5JiTm+LjHWIsE9C4BRIS+MpLi7GbC82N7z2G+yYx8Z8WLVoZT8Obt4THGzeu1Z1lpHXrdhcunJn/7Zx9+2MLCgv8GweEhdVvORFruW62NH4MzWKhLGJpmTol9To4X0wTC4uq1nfV3qm5rLyYYfRKpYMxxc7OHgkKhWjBfoqPzmN8J2q1ury8XKms8oQ4OHD3s6SkuI4s0yuAvXRwcDwZd+yb+V/I5fJu3V4eN+YDT8/6jHewFqVoXohKe4W60MLigsfG2dkjpEnEK93HmiY6Ota1RFKldKRpmVZbZkwp15QgIQEvicoBv4HNxzCHKhWns7KyKm9AsUFnHu6edWSZXoGmaaiR4f+dO4kXL55dE72iuFj99X/rE1bZ8qQH80J0dpNnp5YjYWjs0+zC5b2hwU8bIzpkZCV6edTVCwYb6ebqd+fu1a6VbZK/E04iIYFK0DdEYKNbfx5nhjbYsPDmT127dsWYwh+HNm1WR5bpFaC/3Lz5UyEhTYODQ+F/kbpoz97tqD7Uu7PSrJ2TXivU0AJ4ZBiG2fXHDxpNWVZ28u79Py/4eUh65gOmYLVr3fPq9SMwoALHh09EJ9+LR4KhUXPru8LaOSDMoCjDqp+HRqlUenl5nz9/+n+Xzut0ugH9B/118ujWrZsKiwohZcnS79s/3aFZWDiUrCPLyKHD+6BnHRd3HBqI0JU58dfh1q3aoXpiqbNi3iKGtHGAH19RTrmz55OfjA3d3qkTNx45sW7hshFZ2XeCAlq92X/6AzsfPbuOKi7O27F3wfrfp0PN3u+1Dzdu/lygCFJZSbkKBY+jQVgAAAQmSURBVI6TCxiWYpn6GYihQ979dc2ys+fiNm3cDd6Z7Jys3zav+3nJAvARRj7z7JjRE/lidWQZmfLRjJ8Xfzd95keIW3LuAXX0mwOHofpQR2fFYjSwNXOSGYYO7dQY2R4Jx1J8m6iiJvgizFj68W3/MPuXBkn1S1kz+9aA8f4B4WbaPBbtfMSLrmXFmM6GEhqtRhc1HjsVWjcWp/9HvORyet/99Bt5fi3Mr7bML8j87uchZrPslU6l5eZjnPh6hU4c+wt6csz4qoelLBitkcnMfMDgoLajh1vs690+m+7saofpsk1u9beEJyQ+4rrmDr08zvyRY0mIzk4eH723zmwW9ELs7MzP3KGf9MoXS++BexvacjuFmTauXFZXRLeywvIJc5siPGH5MLBSpl6dFZ5nerjEn8pPupAR8oyZegqMjbtbwzdWnux7uHkiJSDMgcY29KDEp2fX8Rt6gC9gxIwmZYVlBRnCeo8x4V58Di1DURP8ELZY6fRs9DCr+KCeSonPQtZO+t95Rdnq0V8GI5yx0gUr6KEW2MvQhPlN4w8m5aUVIyvl3pX7hdlF8DER5nBzbyQcHxFZ7ms91KeSydDE78PSrmcnnU9HVkfCiZTifPW4uSFIArDVdo+QGpSZCS0V1OPn9f6Cpqxe9/fh5MyEXGQVJF/KBkvv4iofN1cae7pIfTmpYc2N+az6OVPenR185kD+5SN591ML7Z1V3mHujm7SCW5fSW6q+n5SgaZMo3KUDxgX6B8urZhS1tlOrLdXr1MvV/h//s/8+LiC5ItpDMvKFTLuhyrjg7bWLG8ItllzjLFybxnjBjOmmyJVFTYmGksaUwwb2VDVn2jxFWkZy+q5eKGMnmF03Ft0dlf0GhLQpJUElyla6cLmR3QvR/Z0hf9wcOt/6sT4ktzMcm0Zq9cztYUIDmy9ngslawol4+IWG3Y1qizGxTCuVFflveajICNuMSzLL0OsSqEqrlmRYrLzFqRw0Y9N3olcwf1OlPYyd1+7Fh0a+TeV6jJZ1nodOI87zhH2tBP8RwRxsF4/ovWGmrNGFHYyaAghySKXU1yFZTYLEaSDQkWVl0jYfQMN/YBQ871bSXtHbY7gp5zvZwi1hENo4nblQDMdWTDoRIhSousb7vCFHd4oyRHX5GuF3d/0tpSL137NhIch+r93wcvQvpunJNxP6nz24p/ZyTeKRswIdnSx2MAlQpQkmxem5mZo9DoGXGOm6Ub3asWpxdjpJs5ak754Ne9r1UmN3cZrrz2pfm7yqrSM2zfM3knea6hP47C6fjZEiFJGg0pL9dVSeH9q1V725raq54pV7Q9ncmzixDXdyB6x1Q6MTzHuIsZfn9vLnq0YeWArRxpkMvuHc+4RIRKwgLhvCFhAhEjAAiJEAhYQIRKwgAiRgAVEiAQs+H8AAAD//+k+bf0AAAAGSURBVAMASKmUH6ZOP7gAAAAASUVORK5CYII=",
"text/plain": [
"<IPython.core.display.Image object>"
]
},
"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:** `<connector_instantiation>` se distingue de `<orm_connector_init>` (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 <util_command> están ahora completamente expandidas. */\n",
"<util_command> ::= <json_list_cmd> | <crypto_cmd> | <regex_cmd> | <datetime_cmd> | <stamp_cmd> | <string_cmd> | <replace_cmd>\n",
"\n",
"/* Manipulación de listas y JSON */\n",
"<json_list_cmd> ::= \"variableToList(\" <expression> \",\" <identifier> \")\"\n",
" | \"itemFromList(\" <identifier> \",\" <expression> \",\" <identifier> \")\"\n",
" | \"variableFromJSON(\" <identifier> \",\" <expression> \",\" <identifier> \")\"\n",
" | \"AddVariableToJSON(\" <expression> \",\" <expression> \",\" <identifier> \")\"\n",
"\n",
"/* Criptografía */\n",
"<crypto_cmd> ::= \"encodeSHA256(\" <identifier_or_string> \",\" <identifier> \")\"\n",
" | \"encodeMD5(\" <identifier_or_string> \",\" <identifier> \")\"\n",
"\n",
"/* Expresiones regulares */\n",
"<regex_cmd> ::= \"getRegex(\" <identifier> \",\" <stringliteral> \",\" <identifier> \")\"\n",
"\n",
"<datetime_cmd> ::= \"getDateTime(\" <stringliteral> \",\" <expression> \",\" <stringliteral> \",\" <identifier> \")\"\n",
"/* Argumentos: formato_salida, epoch_origen, zona_horaria, destino */\n",
"\n",
"<stamp_cmd> ::= \"stampToDatetime(\" <expression> \",\" <stringliteral> \",\" <expression> \",\" <identifier> \")\"\n",
"/* Argumentos: epoch_origen, formato, timedelta, destino */\n",
" | \"getTimeStamp(\" <stringliteral> \",\" <stringliteral> \",\" <expression> \",\" <identifier> \")\"\n",
"/* Argumentos: fecha_string, formato_entrada, timedelta, destino */\n",
"\n",
"<string_cmd> ::= \"randomString(\" <expression> \",\" <identifier> \")\"\n",
"/* Argumentos: longitud, destino */\n",
"\n",
"<replace_cmd> ::= \"replace(\" <identifier_or_string> \",\" <stringliteral> \",\" <stringliteral> \",\" <identifier> \")\"\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",
"<expression> ::= <logical_or>\n",
"<logical_or> ::= <logical_and> ( \"or\" <logical_and> )*\n",
"<logical_and> ::= <logical_not> ( \"and\" <logical_not> )*\n",
"<logical_not> ::= \"not\" <logical_not> | <comparison>\n",
"\n",
"<comparison> ::= <arithmetic> ( <comp_op> <arithmetic> )*\n",
"<comp_op> ::= \"==\" | \"!=\" | \"<\" | \">\" | \"<=\" | \">=\" | \"in\" | \"is\"\n",
"\n",
"<arithmetic> ::= <term> ( ( \"+\" | \"-\" ) <term> )*\n",
"<term> ::= <factor> ( ( \"*\" | \"/\" | \"%\" ) <factor> )*\n",
"<factor> ::= ( \"+\" | \"-\" ) <factor> | <power>\n",
"<power> ::= <primary> [ \"**\" <factor> ]\n",
"\n",
"/* Primarios y Átomos (Accesos, Castings, Slicing, Métodos y Funciones)\n",
" La regla <primary> 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",
"<primary> ::= <atom>\n",
" | <primary> \".\" <identifier>\n",
" | <primary> \"[\" <expression> \"]\"\n",
" | <primary> \"[\" [<expression>] \":\" [<expression>] [\":\" [<expression>]] \"]\"\n",
" | <primary> \"(\" [<argument_list>] \")\"\n",
"\n",
"<atom> ::= <identifier>\n",
" | \"$\" <identifier>\n",
" | <literal>\n",
" | \"(\" <expression> \")\"\n",
" | <list_display>\n",
" | <dict_display>\n",
"\n",
"/* Estructuras de Datos, Comprensiones y Argumentos */\n",
"<list_display> ::= \"[\" [<argument_list>] \"]\"\n",
" | \"[\" <expression> \"for\" <identifier> \"in\" <expression> [<if_clause>] \"]\"\n",
"<if_clause> ::= \"if\" <expression>\n",
"<dict_display> ::= \"{\" [<key_datum_list>] \"}\"\n",
"<key_datum_list> ::= <key_datum> ( \",\" <key_datum> )*\n",
"<key_datum> ::= <expression> \":\" <expression>\n",
"<argument_list> ::= <expression> ( \",\" <expression> )*\n",
"\n",
"/* Tipo numérico unificado */\n",
"<number> ::= <floatnumber> | <integer>\n",
"\n",
"/* Literales (Tipos de Datos Primitivos Soportados) */\n",
"<literal> ::= <stringliteral> | <number> | <boolean> | \"None\"\n",
"<boolean> ::= \"True\" | \"False\"\n",
"<integer> ::= [0-9]+\n",
"<floatnumber> ::= [0-9]+ \".\" [0-9]* | \".\" [0-9]+\n",
"\n",
"/* Cadenas de Texto con soporte de secuencias de escape */\n",
"<stringliteral> ::= \"\\\"\" <text_double> \"\\\"\" | \"'\" <text_single> \"'\"\n",
"<escape_sequence> ::= \"\\\\\" ( \"\\\"\" | \"'\" | \"\\\\\" | \"n\" | \"t\" | \"r\" | \"0\" )\n",
"<text_double> ::= ( [^\"\\\\] | <escape_sequence> )*\n",
"<text_single> ::= ( [^'\\\\] | <escape_sequence> )*\n",
"<identifier_or_string> ::= <identifier> | <stringliteral>\n",
"\n",
"/* Reglas de Comentarios para el Lexer\n",
" El lexer aplica longest-match: /// debe evaluarse ANTES que // */\n",
"<doc_comment> ::= \"///\" <any_text>\n",
"<line_comment> ::= \"//\" <any_text>\n",
"<block_comment> ::= \"/*\" <any_content> \"*/\"\n",
"<any_text> ::= [^\\r\\n]*\n",
"<any_content> ::= /* 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. **<connector_instantiation>:** This is used to instantiate a connector, which could be for a database connection or a third-party API.\n",
"2. **<orm_connector_init>:** 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
}