{"slug": "from-algorithms-to-agentic-ai-with-python", "title": "From Algorithms to Agentic AI with Python", "summary": "A developer published a step-by-step tutorial showing how to evolve a customer-support assistant from deterministic rule-based code into a full agentic AI system using Python. The progression moves through ten stages, including LLM classification with Groq, tool definitions and execution, vector databases, semantic retrieval, RAG, and finally agent orchestration with Pydantic AI. The writeup argues that introducing one capability at a time makes clear why RAG, tool calling, vector search, and orchestration are each needed.", "body_md": "This tutorial explains a practical progression from traditional deterministic code to an agentic AI system. It follows one consistent example: a customer-support assistant that classifies issues, retrieves user/support knowledge, calls tools, and eventually makes multi-step decisions through an agent framework.\n\nThe goal is not to jump directly into an agent framework. Instead, each stage introduces one new capability so that the reason for RAG, tool calling, vector search, and orchestration becomes clear.\n\nWe will evolve the same support use case through these stages:\n\n```\n1. Algorithmic rules\n      ↓\n2. LLM classification\n      ↓\n3. LLM + tool definitions\n      ↓\n4. LLM + tool execution\n      ↓\n5. LLM + static application data\n      ↓\n6. Vector database\n      ↓\n7. Semantic retrieval\n      ↓\n8. RAG\n      ↓\n9. RAG + tools\n      ↓\n10. Agent orchestration with Pydantic AI\n```\n\nAt the end, the system will be able to:\n\n- understand a user's support request,\n- retrieve relevant knowledge,\n- decide which action is appropriate,\n- execute a Python tool,\n- use the tool result,\n- and produce a final response.\n\n```\nllm_to_agentic_ai/\n├── .env\n├── a1algorithm.py\n├── a2simple_llm.py\n├── a3simple_llm_tool_1.py\n├── a4simple_llm_tool_2.py\n├── a5simple_llm_tool_3.py\n├── a6simple_llm_db_1.py\n├── a6simple_llm_db_2.py\n├── a7simple_llm_db_3.py\n├── a8simple_llm_db_4.py\n├── a9simple_llm_db_5.py\n├── a10simple_llm_db_6.py\n└── a11simple_llm_agent.py\n```\n\nUsing `uv`:\n\n```\nuv add groq python-dotenv pinecone pydantic-ai\n```\n\nIf you also want to demonstrate Chroma locally:\n\n```\nuv add chromadb\n```\n\nOr with pip:\n\n```\npip install groq python-dotenv pinecone pydantic-ai chromadb\n```\n\nCreate a `.env` file:\n\n```\nGROQ_API_KEY=your_groq_key\nPINECONE_API_KEY=your_pinecone_key\n```\n\nNever hard-code production API keys in source code.\n\nClassify support requests using explicit rules.\n\n``` python\ndef clarify_ticket(message: str):\n    message = message.lower()\n\n    if \"password\" in message or \"login\" in message:\n        return \"Authentication Issue\"\n\n    elif \"payment\" in message or \"invoice\" in message:\n        return \"Payment Issue\"\n\n    elif \"slow\" in message or \"performance\" in message:\n        return \"Performance Issue\"\n\n    else:\n        return \"Other\"\n\nprint(clarify_ticket(\"I cannot login\"))\nprint(clarify_ticket(\"My payment failed\"))\nprint(clarify_ticket(\"It is too slow\"))\nprint(clarify_ticket(\"My username is not working\"))\nUser input\n   ↓\nif / elif rules\n   ↓\nCategory\n```\n\n- predictable,\n- cheap,\n- easy to test,\n- deterministic,\n- excellent when rules are known.\n\nThe code only understands patterns you explicitly define.\n\nFor example:\n\n```\n\"I cannot sign in\"\n```\n\nmay mean the same thing as:\n\n```\n\"I cannot login\"\n```\n\nbut your algorithm may not recognize it unless you add another rule.\n\nThis is the first motivation for using an LLM.\n\nLet the model understand the meaning of the request instead of matching keywords.\n\n``` python\nfrom dotenv import load_dotenv\nfrom groq import Groq\n\nload_dotenv()\n\nclient = Groq()\n\ndef clarify_ticket(message: str):\n    completion = client.chat.completions.create(\n        model=\"openai/gpt-oss-120b\",\n        messages=[\n            {\n                \"role\": \"system\",\n                \"content\": \"\"\"\nClassify the support request as one of:\n- Authentication\n- Payment\n- Performance\n- Other\n\nReturn only the category.\n\"\"\"\n            },\n            {\n                \"role\": \"user\",\n                \"content\": message\n            }\n        ],\n        temperature=0,\n        stream=False\n    )\n\n    return completion.choices[0].message.content\n\nprint(clarify_ticket(\"I cannot sign in to my account\"))\nUser input\n   ↓\nLLM understands meaning\n   ↓\nCategory\nDeveloper specifies HOW to decide.\nDeveloper specifies WHAT the desired result is.\nThe model interprets the language.\n```\n\nThe LLM can reason and generate text, but it still cannot perform real application actions by itself.\n\nFor example, it can say:\n\nYou should reset your password.\n\nBut it has not actually reset anything.\n\nThat leads to tools.\n\nGive the model a set of capabilities it may choose from.\n\nFirst create normal Python functions:\n\n``` python\ndef reset_password(username: str):\n    print(f\"Email sent to {username}\")\n    return f\"Password reset link sent to user {username}\"\n\ndef get_invoice(username: str):\n    print(f\"Getting invoice for {username}\")\n    return f\"Latest invoice for user {username}: INV-1024\"\n```\n\nThese functions are ordinary application code.\n\nNow describe them to Groq as tools:\n\n``` python\nfrom groq.types.chat import ChatCompletionToolParam\n\ntools: list[ChatCompletionToolParam] = [\n    {\n        \"type\": \"function\",\n        \"function\": {\n            \"name\": \"reset_password\",\n            \"description\": \"Send a password reset link to a user\",\n            \"parameters\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"username\": {\n                        \"type\": \"string\",\n                        \"description\": \"Username of the user\"\n                    }\n                },\n                \"required\": [\"username\"]\n            }\n        }\n    },\n    {\n        \"type\": \"function\",\n        \"function\": {\n            \"name\": \"get_invoice\",\n            \"description\": \"Get the latest invoice for a user\",\n            \"parameters\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"username\": {\n                        \"type\": \"string\",\n                        \"description\": \"Username of the user\"\n                    }\n                },\n                \"required\": [\"username\"]\n            }\n        }\n    }\n]\n```\n\nThe tool schema does **not** execute the function.\n\nIt only tells the model:\n\n```\nThese capabilities are available.\nHere is what each one does.\nHere are the arguments required.\n```\n\nThe model may then choose a tool.\n\n```\ncompletion = client.chat.completions.create(\n    model=\"openai/gpt-oss-120b\",\n    messages=[\n        {\n            \"role\": \"system\",\n            \"content\": \"\"\"\nAnalyze the user's support request.\n\nCategories:\n- Authentication\n- Payment\n- Performance\n- Other\n\nSelect the most appropriate available tool.\n\"\"\"\n        },\n        {\n            \"role\": \"user\",\n            \"content\": \"I cannot login, my username is dewmal\"\n        }\n    ],\n    tools=tools,\n    tool_choice=\"required\",\n    temperature=0,\n    stream=False\n)\n\nmessage = completion.choices[0].message\n\nif message.tool_calls:\n    selected_tool = message.tool_calls[0]\n\n    print(selected_tool.function.name)\n    print(selected_tool.function.arguments)\n```\n\nPossible output:\n\n```\nreset_password\n{\"username\":\"dewmal\"}\nUser request\n     ↓\n    LLM\n     ↓\nUnderstand intent\n     ↓\nChoose a tool\n     ↓\nTool name + arguments\n```\n\nAt this stage, the model has selected an action, but Python still needs to execute it.\n\nThe tool arguments returned by the model are JSON text.\n\nTherefore this is wrong:\n\n```\nreset_password(**selected_tool.function.arguments)\n```\n\nbecause `arguments` is a string.\n\nConvert it first:\n\n``` python\nimport json\n\narguments = json.loads(\n    selected_tool.function.arguments\n)\n```\n\nThen execute the selected function:\n\n```\nif selected_tool.function.name == \"reset_password\":\n    result = reset_password(**arguments)\n\nelif selected_tool.function.name == \"get_invoice\":\n    result = get_invoice(**arguments)\npython\nimport json\n\ndef clarify_ticket(message: str):\n    completion = client.chat.completions.create(\n        model=\"openai/gpt-oss-120b\",\n        messages=[\n            {\n                \"role\": \"system\",\n                \"content\": \"Select the most appropriate support tool.\"\n            },\n            {\n                \"role\": \"user\",\n                \"content\": message\n            }\n        ],\n        tools=tools,\n        tool_choice=\"required\",\n        temperature=0,\n        stream=False\n    )\n\n    assistant_message = completion.choices[0].message\n\n    if not assistant_message.tool_calls:\n        return assistant_message.content\n\n    selected_tool = assistant_message.tool_calls[0]\n    arguments = json.loads(selected_tool.function.arguments)\n\n    if selected_tool.function.name == \"reset_password\":\n        return reset_password(**arguments)\n\n    if selected_tool.function.name == \"get_invoice\":\n        return get_invoice(**arguments)\n\n    return \"No supported tool selected\"\nLLM = decides WHAT to do\nPython = controls HOW it is done\nTool = capability exposed to the LLM\n```\n\nThis separation is important for security and reliability.\n\nBefore introducing RAG, first demonstrate a simple form of context injection.\n\n```\nusers_db = [\n    {\n        \"username\": \"dewmal\",\n        \"email\": \"dewmal@example.com\",\n        \"status\": \"active\",\n        \"plan\": \"Premium\",\n        \"full_name\": \"Dewmal Handapangoda\"\n    },\n    {\n        \"username\": \"john\",\n        \"email\": \"john@example.com\",\n        \"status\": \"locked\",\n        \"plan\": \"Basic\",\n        \"full_name\": \"John Smith\"\n    },\n    {\n        \"username\": \"sarah\",\n        \"email\": \"sarah@example.com\",\n        \"status\": \"active\",\n        \"plan\": \"Enterprise\",\n        \"full_name\": \"Sarah Kent\"\n    }\n]\n```\n\nInject it into the prompt:\n\n```\ncompletion = client.chat.completions.create(\n    model=\"openai/gpt-oss-120b\",\n    messages=[\n        {\n            \"role\": \"system\",\n            \"content\": f\"\"\"\nYou are a support assistant.\n\nHere is the available user database:\n\n{users_db}\n\nAnswer using only the information above.\nIf the information is unavailable, say you do not know.\n\"\"\"\n        },\n        {\n            \"role\": \"user\",\n            \"content\": \"What plan is John using?\"\n        }\n    ],\n    temperature=0,\n    stream=False\n)\nAll application data\n       ↓\n     Prompt\n       ↓\n      LLM\n       ↓\n     Answer\n```\n\nThis works for small data.\n\n```\n10 records      → easy\n1,000 records   → expensive\n100,000 records → not practical\n1,000,000 docs  → impossible to send every time\n```\n\nProblems include:\n\n- context-window limits,\n- token cost,\n- latency,\n- irrelevant information,\n- increased chance of poor grounding.\n\nThe solution is to retrieve only relevant data.\n\nThis is the reason for vector databases and RAG.\n\nA vector database stores embeddings that represent semantic meaning.\n\nInstead of asking:\n\n```\nDoes this document contain exactly the word \"login\"?\n```\n\nsemantic search can retrieve content related to:\n\n```\nsign in\naccess account\ncredentials not working\ncannot authenticate\n```\n\nbecause the meanings are similar.\n\nFor a completely local demonstration:\n\n``` python\nimport chromadb\n\nchroma_client = chromadb.PersistentClient(\n    path=\"./db\"\n)\n\ncollection = chroma_client.get_or_create_collection(\n    name=\"user_support\"\n)\n```\n\nAdd records only when empty:\n\n```\nif collection.count() == 0:\n    documents = [\n        \"Dewmal has an active Premium account.\",\n        \"John's account is locked after five failed login attempts.\",\n        \"Sarah is an Enterprise customer.\",\n        \"Premium customers receive priority customer support.\",\n        \"Locked accounts require administrator approval.\",\n        \"Refunds normally take five to seven business days.\"\n    ]\n\n    ids = [f\"doc_{i}\" for i in range(len(documents))]\n\n    collection.add(\n        ids=ids,\n        documents=documents\n    )\n```\n\nSearch semantically:\n\n```\nresults = collection.query(\n    query_texts=[\"Why can't John access his account?\"],\n    n_results=2\n)\n\nprint(results[\"documents\"][0])\n```\n\nThe query and retrieved document do not need to contain exactly the same wording.\n\nPinecone is useful for demonstrating a hosted vector database.\n\nAssume you already created an index called:\n\n```\nuser-support\n```\n\nand configured integrated embeddings with the source text field called:\n\n```\ntext\npython\nimport os\nfrom dotenv import load_dotenv\nfrom pinecone import Pinecone\n\nload_dotenv()\n\npc = Pinecone(\n    api_key=os.getenv(\"PINECONE_API_KEY\")\n)\n\nindex_name = \"user-support\"\nnamespace = \"support\"\n\nindex = pc.Index(index_name)\nstats = index.describe_index_stats()\nnamespace_stats = stats.namespaces.get(namespace)\n\ncount = namespace_stats.vector_count if namespace_stats else 0\n\nif count == 0:\n    documents = [\n        \"Dewmal has an active Premium account.\",\n        \"John's account is locked after five failed login attempts.\",\n        \"Sarah is an Enterprise customer.\",\n        \"Premium customers receive priority customer support.\",\n        \"Locked accounts require administrator approval.\",\n        \"Refunds normally take five to seven business days.\"\n    ]\n\n    records = [\n        {\n            \"_id\": f\"doc_{i}\",\n            \"text\": document\n        }\n        for i, document in enumerate(documents)\n    ]\n\n    index.upsert_records(\n        namespace=namespace,\n        records=records\n    )\n```\n\nThe field name used in each record must match the field mapping configured for the Pinecone index.\n\nIf the index expects `text`, this is correct:\n\n```\n{\n    \"_id\": \"doc_1\",\n    \"text\": \"John's account is locked...\"\n}\n```\n\nThis would fail if the index does not use `chunk_text`:\n\n```\n{\n    \"_id\": \"doc_1\",\n    \"chunk_text\": \"John's account is locked...\"\n}\nphp\ndef retrieve_context(question: str) -> str:\n    results = index.search(\n        namespace=namespace,\n        query={\n            \"inputs\": {\n                \"text\": question\n            },\n            \"top_k\": 3\n        },\n        fields=[\"text\"]\n    )\n\n    hits = results[\"result\"][\"hits\"]\n\n    documents = []\n\n    for hit in hits:\n        text = hit[\"fields\"][\"text\"]\n        documents.append(text)\n\n        score = getattr(hit, \"score\", None)\n\n        if score is not None:\n            print(f\"{score:.4f} -> {text}\")\n        else:\n            print(text)\n\n    return \"\\n\".join(documents)\n```\n\nExample:\n\n```\ncontext = retrieve_context(\n    \"Why can't John access his account?\"\n)\n\nprint(context)\n```\n\nPossible relevant results:\n\n```\nJohn's account is locked after five failed login attempts.\nLocked accounts require administrator approval.\n```\n\nThe system did not send the entire database to the LLM.\n\nIt selected only the most relevant pieces first.\n\nRAG has three conceptual parts.\n\n```\nR = Retrieval\nA = Augmentation\nG = Generation\ncontext = retrieve_context(message)\n```\n\nAdd the retrieved context to the model instructions:\n\n```\nmessages = [\n    {\n        \"role\": \"system\",\n        \"content\": f\"\"\"\nYou are a customer support assistant.\n\nAnswer using only this retrieved context:\n\n{context}\n\nIf the answer is unavailable, say you do not know.\n\"\"\"\n    },\n    {\n        \"role\": \"user\",\n        \"content\": message\n    }\n]\ncompletion = groq_client.chat.completions.create(\n    model=\"openai/gpt-oss-120b\",\n    messages=messages,\n    temperature=0,\n    stream=False\n)\npython\ndef clarify_ticket(message: str):\n    context = retrieve_context(message)\n\n    completion = groq_client.chat.completions.create(\n        model=\"openai/gpt-oss-120b\",\n        messages=[\n            {\n                \"role\": \"system\",\n                \"content\": f\"\"\"\nYou are a customer support assistant.\n\nUse only the following retrieved context:\n\n{context}\n\nIf the answer cannot be determined from the context,\nsay \"I don't know based on the available information.\"\n\"\"\"\n            },\n            {\n                \"role\": \"user\",\n                \"content\": message\n            }\n        ],\n        temperature=0,\n        stream=False\n    )\n\n    return completion.choices[0].message.content\nUser question\n     ↓\nVector search\n     ↓\nRelevant documents\n     ↓\nAdd to prompt\n     ↓\nLLM\n     ↓\nGrounded answer\n```\n\nNow we have two powerful capabilities:\n\nGives the model knowledge.\n\nGive the model actions.\n\nThis creates the next architecture:\n\n```\nUser\n ↓\nRetrieve knowledge\n ↓\nLLM understands request + context\n ↓\nChoose tool\n ↓\nExecute Python function\n ↓\nObserve result\n ↓\nGenerate final response\npython\ndef reset_password(username: str):\n    print(f\"[TOOL] Sending password reset email to {username}\")\n\n    return {\n        \"status\": \"success\",\n        \"message\": f\"Password reset link sent to {username}\"\n    }\n\ndef get_invoice(username: str):\n    print(f\"[TOOL] Getting invoice for {username}\")\n\n    return {\n        \"status\": \"success\",\n        \"invoice\": \"INV-1024\",\n        \"username\": username\n    }\n\ndef create_support_ticket(username: str, issue: str):\n    print(f\"[TOOL] Creating ticket for {username}\")\n\n    return {\n        \"status\": \"success\",\n        \"username\": username,\n        \"issue\": issue,\n        \"ticket_id\": \"SUP-1001\"\n    }\n```\n\nDescribe them to Groq with tool schemas.\n\nThen build the support flow.\n\n``` python\nimport json\n\ndef support_agent(message: str):\n    # 1. Retrieve\n    context = retrieve_context(message)\n\n    # 2. Augment\n    messages = [\n        {\n            \"role\": \"system\",\n            \"content\": f\"\"\"\nYou are a customer support assistant.\n\nRelevant retrieved context:\n\n{context}\n\nClassify the request as:\n- Authentication\n- Payment\n- Performance\n- Other\n\nRules:\n- Authentication -> reset_password\n- Payment -> get_invoice\n- Performance -> create_support_ticket\n- Other unresolved issue -> create_support_ticket\n\nSelect the most appropriate tool.\n\"\"\"\n        },\n        {\n            \"role\": \"user\",\n            \"content\": message\n        }\n    ]\n\n    # 3. Model selects tool\n    completion = groq_client.chat.completions.create(\n        model=\"openai/gpt-oss-120b\",\n        messages=messages,\n        tools=tools,\n        tool_choice=\"required\",\n        temperature=0,\n        stream=False\n    )\n\n    assistant_message = completion.choices[0].message\n\n    if not assistant_message.tool_calls:\n        return assistant_message.content\n\n    selected_tool = assistant_message.tool_calls[0]\n\n    tool_name = selected_tool.function.name\n    arguments = json.loads(selected_tool.function.arguments)\n\n    # 4. Execute tool\n    if tool_name == \"reset_password\":\n        tool_result = reset_password(**arguments)\n\n    elif tool_name == \"get_invoice\":\n        tool_result = get_invoice(**arguments)\n\n    elif tool_name == \"create_support_ticket\":\n        tool_result = create_support_ticket(**arguments)\n\n    else:\n        tool_result = {\"error\": \"Unknown tool\"}\n\n    # 5. Ask model to explain the result\n    final_response = groq_client.chat.completions.create(\n        model=\"openai/gpt-oss-120b\",\n        messages=[\n            {\n                \"role\": \"system\",\n                \"content\": f\"\"\"\nYou are a support assistant.\n\nRelevant company context:\n\n{context}\n\nA support action has already been executed.\nExplain the result briefly to the user.\n\"\"\"\n            },\n            {\n                \"role\": \"user\",\n                \"content\": message\n            },\n            {\n                \"role\": \"assistant\",\n                \"content\": f\"\"\"\nSelected action: {tool_name}\nTool result: {json.dumps(tool_result)}\n\"\"\"\n            }\n        ],\n        temperature=0,\n        stream=False\n    )\n\n    return final_response.choices[0].message.content\n```\n\nThe system is no longer just answering questions.\n\nIt is performing a multi-step process:\n\n```\nUnderstand\n   ↓\nRetrieve\n   ↓\nDecide\n   ↓\nAct\n   ↓\nObserve\n   ↓\nRespond\n```\n\nThis begins to resemble agentic behavior.\n\nHowever, we still wrote the orchestration manually.\n\nThat is where an agent framework helps.\n\nIn the manual implementation, we had to manage:\n\n- JSON tool schemas,\n- tool-call inspection,\n- `json.loads` ,\n- `if/elif` function dispatch,\n- tool results,\n- multiple model calls,\n- orchestration logic.\n\nAn agent framework can manage much of that loop.\n\nWith Pydantic AI, ordinary Python functions can become tools through decorators.\n\nWe want the retrieval tool to access Pinecone without relying on global state.\n\n``` python\nfrom dataclasses import dataclass\n\n@dataclass\nclass SupportDeps:\n    index: object\n    namespace: str\npython\nfrom pydantic_ai import Agent, RunContext\n\nagent = Agent(\n    \"groq:openai/gpt-oss-120b\",\n    deps_type=SupportDeps,\n    instructions=\"\"\"\nYou are a customer support agent.\n\nYou have tools for:\n- retrieving company/user knowledge,\n- resetting passwords,\n- checking invoices,\n- creating support tickets.\n\nAlways retrieve relevant support information before deciding how to respond.\n\nAuthentication problems:\n- use reset_password when appropriate.\n\nPayment or invoice problems:\n- use get_invoice.\n\nPerformance or unresolved problems:\n- use create_support_ticket.\n\nKeep the final response short and clear.\n\"\"\"\n)\n```\n\nThis is the major conceptual change.\n\nPreviously the application always executed retrieval first:\n\n```\ncontext = retrieve_context(message)\n```\n\nNow retrieval itself becomes a tool the agent can use.\n\n``` python\n@agent.tool\ndef retrieve_support_context(\n    ctx: RunContext[SupportDeps],\n    question: str\n) -> str:\n    \"\"\"\n    Search the support knowledge base for information\n    relevant to the user's question.\n    \"\"\"\n\n    results = ctx.deps.index.search(\n        namespace=ctx.deps.namespace,\n        query={\n            \"inputs\": {\n                \"text\": question\n            },\n            \"top_k\": 3\n        },\n        fields=[\"text\"]\n    )\n\n    hits = results[\"result\"][\"hits\"]\n\n    documents = []\n\n    for hit in hits:\n        text = hit[\"fields\"][\"text\"]\n        documents.append(text)\n\n    if not documents:\n        return \"No relevant information found.\"\n\n    return \"\\n\".join(documents)\n```\n\nThe LLM can now decide when it needs more knowledge.\n\nThese tools do not need the dependency context, so `tool_plain` is enough.\n\n``` php\n@agent.tool_plain\ndef reset_password(username: str) -> str:\n    \"\"\"Send a password reset link to a user.\"\"\"\n\n    print(f\"[TOOL] Password reset requested for {username}\")\n\n    return f\"Password reset link sent to {username}.\"\nphp\n@agent.tool_plain\ndef get_invoice(username: str) -> str:\n    \"\"\"Get the latest invoice for a user.\"\"\"\n\n    print(f\"[TOOL] Getting invoice for {username}\")\n\n    return f\"Latest invoice for {username}: INV-1024\"\npython\n@agent.tool_plain\ndef create_support_ticket(\n    username: str,\n    issue: str\n) -> str:\n    \"\"\"Create a support ticket for an unresolved issue.\"\"\"\n\n    print(f\"[TOOL] Creating ticket for {username}: {issue}\")\n\n    return (\n        f\"Support ticket SUP-1001 created for \"\n        f\"{username}: {issue}\"\n    )\n```\n\nPydantic AI can derive the tool input schema from the Python function signature.\n\n```\ndeps = SupportDeps(\n    index=index,\n    namespace=namespace\n)\n\ndef support_agent(message: str) -> str:\n    result = agent.run_sync(\n        message,\n        deps=deps\n    )\n\n    return result.output\n```\n\nTest:\n\n```\nprint(\n    support_agent(\n        \"I cannot login, my username is John\"\n    )\n)\n```\n\nConceptually the agent can do this:\n\n```\nUser\n ↓\nAgent receives goal\n ↓\nAgent calls retrieval tool\n ↓\nPinecone returns relevant knowledge\n ↓\nAgent observes result\n ↓\nAgent selects reset_password\n ↓\nPython executes tool\n ↓\nAgent observes tool result\n ↓\nAgent generates final answer\n```\n\nThis is the clearest transition from RAG to agentic AI.\n\n```\nInput\n ↓\nRules\n ↓\nOutput\n```\n\nThe developer decides every branch.\n\n```\nInput\n ↓\nLLM interpretation\n ↓\nOutput\n```\n\nThe model handles ambiguity in language.\n\n```\nInput\n ↓\nLLM\n ↓\nTool selection\n ↓\nPython function\n```\n\nThe model can choose actions.\n\n```\nAll data\n ↓\nPrompt\n ↓\nLLM\n```\n\nThe model can answer using application-specific data, but this does not scale.\n\n```\nQuestion\n ↓\nVector DB\n ↓\nRelevant documents\n```\n\nThe system retrieves based on meaning.\n\n```\nQuestion\n ↓\nRetrieve\n ↓\nAugment prompt\n ↓\nGenerate\n```\n\nThe model answers using relevant private or domain-specific knowledge.\n\n```\nQuestion\n ↓\nRetrieve knowledge\n ↓\nLLM decision\n ↓\nTool execution\n ↓\nFinal answer\n```\n\nThe model has both knowledge and capabilities.\n\n```\nGoal\n ↓\nAgent\n ↓\nChoose next step\n ↓\nRetrieve / Tool / Reason\n ↓\nObserve result\n ↓\nChoose next step\n ↓\nFinish\n```\n\nThe model participates in deciding the sequence of steps.\n\nThe application decides the sequence.\n\nExample:\n\n```\ncontext = retrieve_context(message)\nresult = call_llm(context, message)\nexecute_tool(result)\n```\n\nThe flow is predefined.\n\nThe model can decide the next action from available tools.\n\n```\nNeed knowledge?\n→ retrieve\n\nNeed action?\n→ call tool\n\nEnough information?\n→ answer\n```\n\nAgentic does not mean \"use an LLM everywhere.\" It means the model has controlled decision-making responsibility within a bounded set of capabilities.\n\nUse normal deterministic code when:\n\n- the logic is fixed,\n- compliance requires exact behavior,\n- there are only a few known branches,\n- the action sequence should never vary,\n- latency and cost are critical.\n\nFor example:\n\n```\nif account_locked:\n    require_admin_approval()\n```\n\nmay be much better than asking an LLM to decide.\n\nA strong production system usually combines both:\n\n```\nAI handles ambiguity.\nCode enforces business rules.\n```\n\nThe examples in this tutorial are deliberately small. Real systems need stronger controls.\n\nNever let the model directly execute arbitrary operations.\n\nInstead expose a controlled function:\n\n``` python\ndef reset_password(username: str):\n    # validate identity\n    # check authorization\n    # rate-limit requests\n    # audit action\n    # call trusted backend\n    ...\n```\n\nRetrieval should respect permissions.\n\nA user should not retrieve documents simply because they are semantically similar.\n\nProduction retrieval often needs metadata such as:\n\n```\nuser_id\nteam_id\ntenant_id\ndocument_type\naccess_level\n```\n\nValidate:\n\n- usernames,\n- IDs,\n- JSON,\n- tool parameters,\n- allowed actions,\n- tool results.\n\nBad:\n\n```\n\"Please remember users cannot refund more than $10,000.\"\n```\n\nBetter:\n\n```\nif refund_amount > allowed_limit:\n    reject_refund()\n```\n\nError:\n\n```\nargument after ** must be a mapping, not str\n```\n\nFix:\n\n```\narguments = json.loads(\n    selected_tool.function.arguments\n)\n```\n\nUse the Groq tool type:\n\n``` python\nfrom groq.types.chat import ChatCompletionToolParam\n\ntools: list[ChatCompletionToolParam] = [\n    ...\n]\n```\n\nFor simple tool calling, use:\n\n```\nstream=False\n```\n\nThen:\n\n```\ncompletion.choices[0].message\n```\n\nFor streaming, you must iterate through chunks.\n\n```\n404 NOT_FOUND: Resource user-support not found\n```\n\n`pc.Index(\"user-support\")` assumes that index already exists.\n\nCreate it first in Pinecone or through the API before connecting.\n\nExample error:\n\n```\nMissing field_mapping field 'text'\n```\n\nIf your index expects `text`, records must contain:\n\n```\n{\n    \"_id\": \"doc_0\",\n    \"text\": \"...\"\n}\n```\n\nnot a differently named source field.\n\nDo not assume every SDK response behaves like a plain dictionary.\n\nA defensive pattern is:\n\n```\nscore = getattr(hit, \"score\", None)\n```\n\nThen print it only when available.\n\nA useful live-demo sequence is:\n\nShow:\n\n```\nif \"login\" in message:\n```\n\nThen ask:\n\nWhat happens if the user says \"I can't sign in\"?\n\nShow the model understands varied wording.\n\nKey point:\n\n```\nRules → meaning\n```\n\nShow:\n\n```\nLLM can choose reset_password\n```\n\nbut emphasize that the LLM still has not executed anything.\n\nExecute the Python function.\n\nKey point:\n\n```\nLLM provides intelligence.\nTools provide capabilities.\nPython provides control.\n```\n\nThis works and is intentionally simple.\n\nThen ask:\n\nWhat happens with one million records?\n\nThis creates the need for retrieval naturally.\n\nStore support documents and run semantic search.\n\nShow that:\n\n```\n\"can't access account\"\n```\n\ncan retrieve:\n\n```\n\"account is locked after failed login attempts\"\n```\n\nwithout exact keyword matching.\n\nMap the code explicitly:\n\n```\nR = index.search(...)\nA = retrieved context inserted into prompt\nG = Groq generates answer\n```\n\nShow knowledge and action working together.\n\n```\nRAG = knows\nTools = does\n```\n\nShow how manual orchestration becomes framework-managed tool usage.\n\nKey message:\n\n```\nAgent = model + instructions + tools + controlled loop\n```\n\n| Stage | Who decides? | External knowledge | Can act? | Typical use | \n|---|---|---|---|---|\n| Algorithm | Python rules | No | Yes, deterministic | Fixed business logic | \n| LLM | Model | Only prompt/model knowledge | No | Classification, generation | \n| LLM + Tools | Model chooses tool | Limited | Yes | Intelligent action selection | \n| Static Context | Model | Data copied into prompt | No/optional | Small private datasets | \n| Vector Search | Retrieval system | Vector DB | No | Semantic search | \n| RAG | Retriever + model | Relevant private data | No/optional | Grounded Q&A | \n| RAG + Tools | Retriever + model | Relevant private data | Yes | Knowledge + action | \n| Agentic AI | Model within constraints | Via retrieval/tools | Yes | Dynamic multi-step tasks | \n\nA useful way to explain the entire progression is:\n\n```\nAlgorithm\n= Rules\n\nLLM\n= Language understanding + generation\n\nTools\n= Capabilities\n\nVector DB\n= Searchable semantic memory / knowledge store\n\nRAG\n= Relevant knowledge added at runtime\n\nAgent\n= LLM that can decide how and when to use tools and knowledge\n```\n\nOr even more simply:\n\n```\nLLM gives intelligence.\nRAG gives knowledge.\nTools give actions.\nAgent orchestration connects them.\n```\n\n1. Start with deterministic code when the logic is fixed.\n2. Add an LLM when you need language understanding or flexible interpretation.\n3. Add tools when the model needs to perform real actions.\n4. Add external context when the model needs private or domain-specific information.\n5. Do not send all data to the model when the dataset grows.\n6. Use vector retrieval to find only relevant information.\n7. RAG combines retrieval with generation.\n8. Combine RAG with tools when the system must both know and act.\n9. Use an agent framework when the model needs controlled multi-step decision-making.\n10. Keep authorization, validation, security, and critical business rules in deterministic application code.\n\nThe most important engineering principle is:\n\n**Building an AI demo is easy. Building a reliable, secure, testable, and maintainable AI system is software engineering.**", "url": "https://wpnews.pro/news/from-algorithms-to-agentic-ai-with-python", "canonical_source": "https://gist.github.com/dewmal/e4bc7b8fdf418de4730c0ebdde3568b2", "published_at": "2026-09-16 06:33:00+00:00", "updated_at": "2026-09-19 10:54:37.216532+00:00", "lang": "en", "topics": ["ai-agents", "large-language-models", "ai-tools", "developer-tools", "ai-products"], "entities": ["Python", "Groq", "Pinecone", "Pydantic AI", "Chroma", "uv"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/from-algorithms-to-agentic-ai-with-python", "markdown": "https://wpnews.pro/news/from-algorithms-to-agentic-ai-with-python.md", "text": "https://wpnews.pro/news/from-algorithms-to-agentic-ai-with-python.txt", "jsonld": "https://wpnews.pro/news/from-algorithms-to-agentic-ai-with-python.jsonld"}}