diff --git a/scratches/pseco/agent/n00 LangGraph Agent v2.ipynb b/scratches/pseco/agent/n00 LangGraph Agent v2.ipynb index 3d005da..d3840e0 100644 --- a/scratches/pseco/agent/n00 LangGraph Agent v2.ipynb +++ b/scratches/pseco/agent/n00 LangGraph Agent v2.ipynb @@ -1,455 +1,414 @@ { - "cells": [ - { - "cell_type": "markdown", - "id": "9f97dd1e", - "metadata": {}, - "source": [ - "# Libraries" - ] - }, - { - "cell_type": "code", - "execution_count": 83, - "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" - ] - }, - { - "cell_type": "code", - "execution_count": 84, - "id": "30edcecc", - "metadata": {}, - "outputs": [], - "source": [ - "ES_URL = os.getenv(\"ELASTICSEARCH_LOCAL_URL\")\n", - "INDEX_NAME = os.getenv(\"ELASTICSEARCH_INDEX\")\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": null, - "id": "5f8c88cf", - "metadata": {}, - "outputs": [], - "source": [ - "class AgentState(TypedDict):\n", - " messages: Annotated[list, add_messages]\n", - " language_ok: bool\n", - " language_retries: int" - ] - }, - { - "cell_type": "markdown", - "id": "1d60c120", - "metadata": {}, - "source": [ - "### Tools" - ] - }, - { - "cell_type": "code", - "execution_count": 86, - "id": "f0a21230", - "metadata": {}, - "outputs": [], - "source": [ - "retrieve_kwargs = {\"k\": 5}" - ] - }, - { - "cell_type": "code", - "execution_count": 87, - "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": 88, - "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": 89, - "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": null, - "id": "36d0f54e", - "metadata": {}, - "outputs": [], - "source": [ - "def _safe_detect_language(text: str) -> str:\n", - " stripped_text = (text or \"\").strip()\n", - " if not stripped_text:\n", - " return \"unknown\"\n", - "\n", - " try:\n", - " from langdetect import LangDetectException, detect\n", - " return detect(stripped_text)\n", - " except Exception:\n", - " cjk_pattern = r\"[\\u3400-\\u4dbf\\u4e00-\\u9fff\\uf900-\\ufaff\\u3040-\\u30ff\\uac00-\\ud7af]\"\n", - " if re.search(cjk_pattern, stripped_text):\n", - " return \"non-en\"\n", - " ascii_ratio = sum(1 for char in stripped_text if ord(char) < 128) / max(len(stripped_text), 1)\n", - " return \"en\" if ascii_ratio > 0.9 else \"unknown\"\n", - "\n", - "\n", - "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 = 2\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", - " model = llm.bind_tools(tools)\n", - " response = model.invoke([system, *messages])\n", - "\n", - " return {\"messages\": [*messages, response]}\n", - "\n", - "\n", - "def language_guard(state: AgentState) -> AgentState:\n", - " messages: List[BaseMessage] = state[\"messages\"]\n", - " retries = state.get(\"language_retries\", 0)\n", - " assistant_text = _last_assistant_text(messages)\n", - " detected_language = _safe_detect_language(assistant_text)\n", - " is_english = detected_language == \"en\"\n", - "\n", - " if is_english or retries >= MAX_LANGUAGE_RETRIES:\n", - " return {\n", - " \"messages\": messages,\n", - " \"language_ok\": is_english,\n", - " \"language_retries\": retries,\n", - " }\n", - "\n", - " correction_instruction = SystemMessage(\n", - " content=(\n", - " \"Your previous answer was not in English. Regenerate the final answer in English only. \"\n", - " \"Keep the same meaning and format requirements.\"\n", - " )\n", - " )\n", - " return {\n", - " \"messages\": [*messages, correction_instruction],\n", - " \"language_ok\": False,\n", - " \"language_retries\": retries + 1,\n", - " }\n", - "\n", - "\n", - "def route_after_language_guard(state: AgentState) -> str:\n", - " if state.get(\"language_ok\", False):\n", - " return \"end\"\n", - " if state.get(\"language_retries\", 0) >= MAX_LANGUAGE_RETRIES:\n", - " return \"end\"\n", - " return \"retry\"" - ] - }, - { - "cell_type": "markdown", - "id": "ef55bca3", - "metadata": {}, - "source": [ - "### Graph" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fae46a58", - "metadata": {}, - "outputs": [], - "source": [ - "graph = StateGraph(AgentState)\n", - "graph.add_node(\"agent\", agent)\n", - "graph.add_node(\"language_guard\", language_guard)\n", - "\n", - "graph.set_entry_point(\"agent\")\n", - "graph.add_edge(\"agent\", \"language_guard\")\n", - "graph.add_conditional_edges(\n", - " \"language_guard\",\n", - " route_after_language_guard,\n", - " {\"retry\": \"agent\", \"end\": END},\n", - ")\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": 92, - "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": 93, - "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": 94, - "id": "a1a1f3cf", - "metadata": {}, - "outputs": [], - "source": [ - "user_input = (\n", - " \"Create exactly two variables in AVAP. \"\n", - " \"Each variable must be assigned a single integer number using addVar(). \"\n", - " \"For example: first variable named x with value 10, second variable named y with value 20. \"\n", - " \"Return only the AVAP code snippet with the two addVar() statements in one fenced ```avap``` block. \"\n", - " \"Do not add explanations, just the code.\"\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 95, - "id": "53b89690", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "================================\u001b[1m Human Message \u001b[0m=================================\n", - "\n", - "Create exactly two variables in AVAP. Each variable must be assigned a single integer number using addVar(). For example: first variable named x with value 10, second variable named y with value 20. Return only the AVAP code snippet with the two addVar() statements in one fenced ```avap``` block. Do not add explanations, just the code.\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "\n", - "Okay, the user wants me to create exactly two variables in the AVAP language. Each variable needs to be an integer, and they should be assigned using addVar(). The example given is first variable x with value 10, second y with 20. I need to return the code in a single ```avap``` block without any explanations.\n", - "\n", - "First, I should make sure I understand the task correctly. The user specified two variables, each assigned an integer. The function to use is addVar(), and the variables should be named x and y. The example uses x and y, so I'll follow that format.\n", - "\n", - "I need to create two addVar() statements. The first one would be addVar(\"x\", 10), and the second would be addVar(\"y\", 20). Then, I have to put all this into a single ```avap``` block. Let me check the syntax again to make sure. The function call should be in JSON within the tool_call tags. Each addVar is a separate function call. \n", - "\n", - "Wait, the user said \"return only the AVAP code snippet with the two addVar() statements\". So the code should be two lines: first the addVar for x, then another addVar for y. No explanations, just the code. Alright, that's clear. I'll structure the tool calls correctly, ensuring that each function call is properly formatted.\n", - "\n", - "\n", - "```avap\n", - "addVar(x, 10)\n", - "addVar(y, 20)\n", - "```\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 + "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": 146, + "id": "30edcecc", + "metadata": {}, + "outputs": [], + "source": [ + "ES_URL = os.getenv(\"ELASTICSEARCH_LOCAL_URL\")\n", + "INDEX_NAME = os.getenv(\"ELASTICSEARCH_INDEX\")\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/src/config.py b/src/config.py new file mode 100644 index 0000000..952b935 --- /dev/null +++ b/src/config.py @@ -0,0 +1,36 @@ +from pathlib import Path +from dotenv import load_dotenv +import os + +load_dotenv() + +OPENAI_API_KEY=os.getenv("OPENAI_API_KEY", "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") + +OLLAMA_URL=os.getenv("OLLAMA_URL", "http://host.docker.internal:11434") +OLLAMA_LOCAL_URL=os.getenv("OLLAMA_LOCAL_URL", "http://localhost:11434") +OLLAMA_MODEL_NAME=os.getenv("OLLAMA_MODEL_NAME", "qwen3-0.6B:latest") +OLLAMA_EMB_MODEL_NAME=os.getenv("OLLAMA_EMB_MODEL_NAME", "qwen3-0.6B-emb:latest") + +LANGFUSE_HOST=os.getenv("LANGFUSE_HOST", "http://45.77.119.180") +LANGFUSE_PUBLIC_KEY=os.getenv("LANGFUSE_PUBLIC_KEY", "pk-lf-0e6db694-3e95-4dd4-aedf-5a2694267058") +LANGFUSE_SECRET_KEY=os.getenv("LANGFUSE_SECRET_KEY", "sk-lf-dbf28bb9-15bb-4d03-a8c3-05caa3e3905f") + +ELASTICSEARCH_URL=os.getenv("ELASTICSEARCH_URL", "http://host.docker.internal:9200") +ELASTICSEARCH_LOCAL_URL=os.getenv("ELASTICSEARCH_LOCAL_URL", "http://localhost:9200") +ELASTICSEARCH_INDEX=os.getenv("ELASTICSEARCH_INDEX", "avap-docs-test") + +DATABASE_URL=os.getenv("DATABASE_URL", "postgresql://postgres:brunix_pass@host.docker.internal:5432/postgres") + +KUBECONFIG_PATH=os.getenv("KUBECONFIG_PATH", "kubernetes/kubeconfig.yaml") + +HF_TOKEN=os.getenv("HF_TOKEN", "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") +HF_EMB_MODEL_NAME=os.getenv("HF_EMB_MODEL_NAME", "Qwen/Qwen3-Embedding-0.6B") + +PROJ_ROOT = Path(__file__).resolve().parents[1] + +DATA_DIR=PROJ_ROOT / "data" +MODELS_DIR=DATA_DIR / "models" +RAW_DIR=DATA_DIR / "raw" +PROCESSED_DIR=DATA_DIR / "processed" +INTERIM_DIR=DATA_DIR / "interim" +EXTERNAL_DIR=DATA_DIR / "external" \ No newline at end of file diff --git a/src/llm_factory v1.py b/src/llm_factory v1.py new file mode 100644 index 0000000..0a3b1a9 --- /dev/null +++ b/src/llm_factory v1.py @@ -0,0 +1,152 @@ +from __future__ import annotations + +from dataclasses import dataclass +from enum import StrEnum +from typing import Optional + +from langchain_ollama import ChatOllama, OllamaEmbeddings + + +class Provider(StrEnum): + OLLAMA = "ollama" + OPENAI = "openai" + ANTHROPIC = "anthropic" + AWS_BEDROCK = "aws_bedrock" + HUGGINGFACE = "huggingface" + + +@dataclass(frozen=True) +class ChatModelConfig: + provider: Provider + model: str + temperature: float = 0.0 + + # Ollama + ollama_base_url: Optional[str] = None + validate_model_on_init: bool = True + + # OpenAI / Anthropic / Azure + api_key: Optional[str] = None + azure_endpoint: Optional[str] = None + azure_deployment: Optional[str] = None + api_version: Optional[str] = None + + +@dataclass(frozen=True) +class EmbeddingsConfig: + provider: Provider + model: str + + # Ollama + ollama_base_url: Optional[str] = None + + # OpenAI / Azure + api_key: Optional[str] = None + azure_endpoint: Optional[str] = None + azure_deployment: Optional[str] = None + api_version: Optional[str] = None + + +def build_chat_model(cfg: ChatModelConfig): + match cfg.provider: + case Provider.OLLAMA: + return ChatOllama( + model=cfg.model, + temperature=cfg.temperature, + validate_model_on_init=cfg.validate_model_on_init, + base_url=cfg.ollama_base_url, + ) + + case Provider.OPENAI: + from langchain_openai import ChatOpenAI # pip install langchain-openai + + if not cfg.api_key: + raise ValueError("Missing api_key for OpenAI provider.") + return ChatOpenAI( + model=cfg.model, + temperature=cfg.temperature, + api_key=cfg.api_key, + ) + + case Provider.ANTHROPIC: + from langchain_anthropic import ChatAnthropic # pip install langchain-anthropic + + if not cfg.api_key: + raise ValueError("Missing api_key for Anthropic provider.") + return ChatAnthropic( + model=cfg.model, + temperature=cfg.temperature, + api_key=cfg.api_key, + ) + + case Provider.AZURE_OPENAI: + from langchain_openai import AzureChatOpenAI # pip install langchain-openai + + missing = [ + name + for name, value in { + "api_key": cfg.api_key, + "azure_endpoint": cfg.azure_endpoint, + "azure_deployment": cfg.azure_deployment, + "api_version": cfg.api_version, + }.items() + if not value + ] + if missing: + raise ValueError(f"Missing Azure settings: {', '.join(missing)}") + + return AzureChatOpenAI( + api_key=cfg.api_key, + azure_endpoint=cfg.azure_endpoint, + azure_deployment=cfg.azure_deployment, + api_version=cfg.api_version, + temperature=cfg.temperature, + ) + + case _: + raise ValueError(f"Unsupported provider: {cfg.provider}") + + +def build_embeddings(cfg: EmbeddingsConfig): + match cfg.provider: + case Provider.OLLAMA: + return OllamaEmbeddings( + model=cfg.model, + base_url=cfg.ollama_base_url, + ) + + case Provider.OPENAI: + from langchain_openai import OpenAIEmbeddings # pip install langchain-openai + + if not cfg.api_key: + raise ValueError("Missing api_key for OpenAI embeddings provider.") + return OpenAIEmbeddings( + model=cfg.model, + api_key=cfg.api_key, + ) + + case Provider.AZURE_OPENAI: + from langchain_openai import AzureOpenAIEmbeddings # pip install langchain-openai + + missing = [ + name + for name, value in { + "api_key": cfg.api_key, + "azure_endpoint": cfg.azure_endpoint, + "azure_deployment": cfg.azure_deployment, + "api_version": cfg.api_version, + }.items() + if not value + ] + if missing: + raise ValueError(f"Missing Azure settings: {', '.join(missing)}") + + return AzureOpenAIEmbeddings( + api_key=cfg.api_key, + azure_endpoint=cfg.azure_endpoint, + azure_deployment=cfg.azure_deployment, + api_version=cfg.api_version, + ) + + case _: + raise ValueError(f"Unsupported embeddings provider: {cfg.provider}") \ No newline at end of file diff --git a/src/llm_factory v2.py b/src/llm_factory v2.py new file mode 100644 index 0000000..2c89d33 --- /dev/null +++ b/src/llm_factory v2.py @@ -0,0 +1,179 @@ +from __future__ import annotations + +from dataclasses import dataclass +from enum import StrEnum +from typing import Optional + + +# ---------- Providers ---------- +class Provider(StrEnum): + OLLAMA = "ollama" + OPENAI = "openai" + ANTHROPIC = "anthropic" + AWS_BEDROCK = "aws_bedrock" + HUGGINGFACE = "huggingface" + + +# ---------- Provider-specific configs ---------- +@dataclass(frozen=True) +class OllamaCfg: + base_url: Optional[str] = None + validate_model_on_init: bool = True + + +@dataclass(frozen=True) +class OpenAICfg: + api_key: str + + +@dataclass(frozen=True) +class AnthropicCfg: + api_key: str + + +@dataclass(frozen=True) +class BedrockCfg: + # depende de cómo autentiques: env vars, perfil AWS, role, etc. + region_name: Optional[str] = None + # model_kwargs típicos: temperature, max_tokens, etc. (según wrapper) + # lo dejamos mínimo para no acoplar + pass + + +@dataclass(frozen=True) +class HuggingFaceCfg: + # puede ser token HF o endpoint, según uses Inference API o local + api_key: Optional[str] = None + endpoint_url: Optional[str] = None + + +# ---------- Base configs ---------- +@dataclass(frozen=True) +class ChatModelConfig: + provider: Provider + model: str + temperature: float = 0.0 + + # EXACTAMENTE una de estas debería venir informada según provider: + ollama: Optional[OllamaCfg] = None + openai: Optional[OpenAICfg] = None + anthropic: Optional[AnthropicCfg] = None + bedrock: Optional[BedrockCfg] = None + huggingface: Optional[HuggingFaceCfg] = None + + +@dataclass(frozen=True) +class EmbeddingsConfig: + provider: Provider + model: str + + ollama: Optional[OllamaCfg] = None + openai: Optional[OpenAICfg] = None + bedrock: Optional[BedrockCfg] = None + huggingface: Optional[HuggingFaceCfg] = None + + +# ---------- Helpers ---------- +def _require(value, msg: str): + if value is None: + raise ValueError(msg) + return value + + +def _require_cfg(cfg_obj, msg: str): + if cfg_obj is None: + raise ValueError(msg) + return cfg_obj + + +# ---------- Builders ---------- +def build_chat_model(cfg: ChatModelConfig): + match cfg.provider: + case Provider.OLLAMA: + from langchain_ollama import ChatOllama + + ocfg = cfg.ollama or OllamaCfg() + return ChatOllama( + model=cfg.model, + temperature=cfg.temperature, + validate_model_on_init=ocfg.validate_model_on_init, + base_url=ocfg.base_url, + ) + + case Provider.OPENAI: + from langchain_openai import ChatOpenAI # pip install langchain-openai + + ocfg = _require_cfg(cfg.openai, "Missing cfg.openai for OpenAI provider.") + return ChatOpenAI( + model=cfg.model, + temperature=cfg.temperature, + api_key=ocfg.api_key, + ) + + case Provider.ANTHROPIC: + from langchain_anthropic import ChatAnthropic # pip install langchain-anthropic + + acfg = _require_cfg(cfg.anthropic, "Missing cfg.anthropic for Anthropic provider.") + return ChatAnthropic( + model=cfg.model, + temperature=cfg.temperature, + api_key=acfg.api_key, + ) + + case Provider.AWS_BEDROCK: + # wrapper típico: langchain-aws (según versión) o langchain-community en algunos setups + # aquí lo dejo como ejemplo con guardrail claro + try: + from langchain_aws import ChatBedrock # pip install langchain-aws + except Exception as e: + raise ImportError( + "To use AWS Bedrock, install `langchain-aws` and configure AWS credentials." + ) from e + + bcfg = cfg.bedrock or BedrockCfg() + # OJO: ChatBedrock suele usar model_id en vez de model, depende del wrapper/versión. + return ChatBedrock( + model_id=cfg.model, + region_name=bcfg.region_name, + model_kwargs={"temperature": cfg.temperature}, + ) + + case Provider.HUGGINGFACE: + # depende MUCHO: endpoint, local pipeline, inference API... + raise NotImplementedError( + "HUGGINGFACE provider not implemented here (depends on whether you use Inference API, TGI, or local pipeline)." + ) + + case _: + raise ValueError(f"Unsupported provider: {cfg.provider}") + + +def build_embeddings(cfg: EmbeddingsConfig): + match cfg.provider: + case Provider.OLLAMA: + from langchain_ollama import OllamaEmbeddings + + ocfg = cfg.ollama or OllamaCfg() + return OllamaEmbeddings( + model=cfg.model, + base_url=ocfg.base_url, + ) + + case Provider.OPENAI: + from langchain_openai import OpenAIEmbeddings # pip install langchain-openai + + ocfg = _require_cfg(cfg.openai, "Missing cfg.openai for OpenAI embeddings provider.") + return OpenAIEmbeddings( + model=cfg.model, + api_key=ocfg.api_key, + ) + + case Provider.AWS_BEDROCK: + # Igual: depende del wrapper + raise NotImplementedError("Bedrock embeddings: añade el wrapper que uses y mapea aquí.") + + case Provider.HUGGINGFACE: + raise NotImplementedError("HuggingFace embeddings: depende del wrapper (endpoint/local).") + + case _: + raise ValueError(f"Unsupported embeddings provider: {cfg.provider}") \ No newline at end of file