{"id":424,"date":"2026-02-17T06:01:30","date_gmt":"2026-02-16T22:01:30","guid":{"rendered":"https:\/\/connectword.dpdns.org\/?p=424"},"modified":"2026-02-17T06:01:30","modified_gmt":"2026-02-16T22:01:30","slug":"how-to-build-human-in-the-loop-plan-and-execute-ai-agents-with-explicit-user-approval-using-langgraph-and-streamlit","status":"publish","type":"post","link":"https:\/\/connectword.dpdns.org\/?p=424","title":{"rendered":"How to Build Human-in-the-Loop Plan-and-Execute AI Agents with Explicit User Approval Using LangGraph and Streamlit"},"content":{"rendered":"<p>In this tutorial, we build a human-in-the-loop travel booking agent that treats the user as a teammate rather than a passive observer. We design the system so the agent first reasons openly by drafting a structured travel plan, then deliberately pauses before taking any action. We expose this proposed plan in a live interface where we can inspect, edit, or reject it, and only after explicit approval do we allow the agent to execute tools. By combining LangGraph interrupts with a Streamlit frontend, we create a workflow that makes agent reasoning visible, controllable, and trustworthy instead of opaque and autonomous.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\" no-line-numbers\"><code class=\" no-wrap language-php\">!pip -q install -U langgraph openai streamlit pydantic\n!npm -q install -g localtunnel\n\n\nimport os, getpass, textwrap, json, uuid, time\nif not os.environ.get(\"OPENAI_API_KEY\"):\n   os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OPENAI_API_KEY (hidden input): \")\nos.environ.setdefault(\"OPENAI_MODEL\", \"gpt-4.1-mini\")<\/code><\/pre>\n<\/div>\n<\/div>\n<p>We set up the execution environment by installing all required libraries and utilities needed for agent orchestration and UI exposure. We securely collect the OpenAI API key at runtime so it is never hardcoded or leaked in the notebook. We also configure the model selection upfront to keep the rest of the pipeline clean and reproducible.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\" no-line-numbers\"><code class=\" no-wrap language-php\">app_code = r'''\nimport os, json, uuid\nimport streamlit as st\nfrom typing import TypedDict, List, Dict, Any, Optional\nfrom pydantic import BaseModel, Field\nfrom openai import OpenAI\n\n\nfrom langgraph.graph import StateGraph, START, END\nfrom langgraph.types import Command, interrupt\nfrom langgraph.checkpoint.memory import InMemorySaver\n\n\n\n\ndef tool_search_flights(origin: str, destination: str, depart_date: str, return_date: str, budget_usd: int) -&gt; Dict[str, Any]:\n   options = [\n       {\"airline\": \"SkyJet\", \"route\": f\"{origin}-&gt;{destination}\", \"depart\": depart_date, \"return\": return_date, \"price_usd\": int(budget_usd*0.55)},\n       {\"airline\": \"AeroBlue\", \"route\": f\"{origin}-&gt;{destination}\", \"depart\": depart_date, \"return\": return_date, \"price_usd\": int(budget_usd*0.70)},\n       {\"airline\": \"Nimbus Air\", \"route\": f\"{origin}-&gt;{destination}\", \"depart\": depart_date, \"return\": return_date, \"price_usd\": int(budget_usd*0.62)},\n   ]\n   options = sorted(options, key=lambda x: x[\"price_usd\"])\n   return {\"tool\": \"search_flights\", \"top_options\": options[:2]}\n\n\ndef tool_search_hotels(city: str, nights: int, budget_usd: int, preferences: List[str]) -&gt; Dict[str, Any]:\n   base = max(60, int(budget_usd \/ max(nights, 1)))\n   picks = [\n       {\"name\": \"Central Boutique\", \"city\": city, \"nightly_usd\": int(base*0.95), \"notes\": [\"walkable\", \"great reviews\"]},\n       {\"name\": \"Riverside Stay\", \"city\": city, \"nightly_usd\": int(base*0.80), \"notes\": [\"quiet\", \"good value\"]},\n       {\"name\": \"Modern Loft Hotel\", \"city\": city, \"nightly_usd\": int(base*1.10), \"notes\": [\"new\", \"gym\"]},\n   ]\n   if \"luxury\" in [p.lower() for p in preferences]:\n       picks = sorted(picks, key=lambda x: -x[\"nightly_usd\"])\n   else:\n       picks = sorted(picks, key=lambda x: x[\"nightly_usd\"])\n   return {\"tool\": \"search_hotels\", \"top_options\": picks[:2]}\n\n\ndef tool_build_day_by_day(city: str, days: int, vibe: str) -&gt; Dict[str, Any]:\n   blocks = []\n   for d in range(1, days+1):\n       blocks.append({\n           \"day\": d,\n           \"morning\": f\"{city}: coffee + a must-see landmark\",\n           \"afternoon\": f\"{city}: {vibe} activity + local lunch\",\n           \"evening\": f\"{city}: sunset spot + dinner + optional night walk\"\n       })\n   return {\"tool\": \"draft_itinerary\", \"days\": blocks}\n'''\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p>We define the Streamlit application core and implement safe, deterministic tool functions that simulate flights, hotels, and itinerary generation. We design these tools to behave like real-world APIs while still running fully in a Colab environment. We ensure all tool outputs are structured so they can be audited before execution.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\" no-line-numbers\"><code class=\" no-wrap language-php\">app_code += r'''\nclass TravelPlan(BaseModel):\n   trip_title: str = Field(..., description=\"Short human-friendly title\")\n   origin: str\n   destination: str\n   depart_date: str\n   return_date: str\n   travelers: int = 1\n   budget_usd: int = 1500\n   preferences: List[str] = Field(default_factory=list)\n   vibe: str = \"balanced\"\n   lodging_nights: int = 4\n   daily_outline: List[Dict[str, Any]] = Field(default_factory=list)\n   tool_calls: List[Dict[str, Any]] = Field(default_factory=list)\n\n\nclass State(TypedDict):\n   user_request: str\n   plan: Dict[str, Any]\n   approval: Dict[str, Any]\n   execution: Dict[str, Any]\n\n\ndef make_llm_plan(state: State) -&gt; Dict[str, Any]:\n   client = OpenAI(api_key=os.environ[\"OPENAI_API_KEY\"])\n   model = os.environ.get(\"OPENAI_MODEL\", \"gpt-4.1-mini\")\n\n\n   sys = (\n       \"You are a travel planning agent. \"\n       \"Return a JSON travel plan that matches the provided schema. \"\n       \"Be realistic, concise, and include a tool_calls list describing what you want executed \"\n       \"(e.g., search_flights, search_hotels, draft_itinerary).\"\n   )\n\n\n   schema = TravelPlan.model_json_schema()\n\n\n   resp = client.responses.create(\n       model=model,\n       input=[\n           {\"role\":\"system\",\"content\": sys},\n           {\"role\":\"user\",\"content\": state[\"user_request\"]},\n           {\"role\":\"user\",\"content\": f\"Schema (JSON): {json.dumps(schema)}\"}\n       ],\n   )\n\n\n   text = resp.output_text.strip()\n   start = text.find(\"{\")\n   end = text.rfind(\"}\")\n   if start == -1 or end == -1:\n       raise ValueError(\"Model did not return JSON. Try again or change model.\")\n   raw = text[start:end+1]\n   plan_obj = json.loads(raw)\n\n\n   plan = TravelPlan(**plan_obj).model_dump()\n\n\n   if not plan.get(\"tool_calls\"):\n       plan[\"tool_calls\"] = [\n           {\"name\":\"search_flights\", \"args\":{\"origin\": plan[\"origin\"], \"destination\": plan[\"destination\"], \"depart_date\": plan[\"depart_date\"], \"return_date\": plan[\"return_date\"], \"budget_usd\": plan[\"budget_usd\"]}},\n           {\"name\":\"search_hotels\", \"args\":{\"city\": plan[\"destination\"], \"nights\": plan[\"lodging_nights\"], \"budget_usd\": int(plan[\"budget_usd\"]*0.35), \"preferences\": plan[\"preferences\"]}},\n           {\"name\":\"draft_itinerary\", \"args\":{\"city\": plan[\"destination\"], \"days\": max(2, plan[\"lodging_nights\"]+1), \"vibe\": plan[\"vibe\"]}},\n       ]\n\n\n   return {\"plan\": plan}\n\n\ndef wait_for_approval(state: State) -&gt; Dict[str, Any]:\n   payload = {\n       \"kind\": \"approval\",\n       \"message\": \"Review\/edit the plan. Approve to execute tools.\",\n       \"plan\": state[\"plan\"],\n   }\n   decision = interrupt(payload)\n   return {\"approval\": decision}\n\n\ndef execute_tools(state: State) -&gt; Dict[str, Any]:\n   approval = state.get(\"approval\") or {}\n   if not approval.get(\"approved\"):\n       return {\"execution\": {\"status\": \"not_executed\", \"reason\": \"User rejected or did not approve.\"}}\n\n\n   plan = approval.get(\"edited_plan\") or state[\"plan\"]\n   tool_calls = plan.get(\"tool_calls\", [])\n\n\n   results = []\n   for call in tool_calls:\n       name = call.get(\"name\")\n       args = call.get(\"args\", {})\n       if name == \"search_flights\":\n           results.append(tool_search_flights(**args))\n       elif name == \"search_hotels\":\n           results.append(tool_search_hotels(**args))\n       elif name == \"draft_itinerary\":\n           results.append(tool_build_day_by_day(**args))\n       else:\n           results.append({\"tool\": name, \"error\": \"Unknown tool (blocked for safety).\", \"args\": args})\n\n\n   return {\"execution\": {\"status\": \"executed\", \"tool_results\": results, \"final_plan\": plan}}\n'''\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p>We formalize the agent\u2019s reasoning using a strict schema that requires the model to output an explicit travel plan rather than free-form text. We generate the plan using the OpenAI model and validate it before allowing it into the workflow. We also auto-inject tool calls if the model omits them to guarantee a complete execution path.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\" no-line-numbers\"><code class=\" no-wrap language-php\">app_code += r'''\ndef build_graph():\n   builder = StateGraph(State)\n   builder.add_node(\"plan\", make_llm_plan)\n   builder.add_node(\"approve\", wait_for_approval)\n   builder.add_node(\"execute\", execute_tools)\n\n\n   builder.add_edge(START, \"plan\")\n   builder.add_edge(\"plan\", \"approve\")\n   builder.add_edge(\"approve\", \"execute\")\n   builder.add_edge(\"execute\", END)\n\n\n   memory = InMemorySaver()\n   graph = builder.compile(checkpointer=memory)\n   return graph\n\n\nst.set_page_config(page_title=\"Plan \u2192 Approve \u2192 Execute Travel Agent\", layout=\"wide\")\nst.title(\"Human-in-the-Loop Travel Booking Agent (Plan \u2192 Approve\/Edit \u2192 Execute)\")\n\n\nwith st.sidebar:\n   st.header(\"Runtime\")\n   if st.button(\"New Session \/ Thread\"):\n       st.session_state.thread_id = str(uuid.uuid4())\n       st.session_state.ran_once = False\n       st.session_state.interrupt_payload = None\n       st.session_state.last_execution = None\n\n\nthread_id = st.session_state.get(\"thread_id\") or str(uuid.uuid4())\nst.session_state.thread_id = thread_id\n\n\ngraph = build_graph()\nconfig = {\"configurable\": {\"thread_id\": thread_id}}\n\n\nst.caption(f\"Thread ID: {thread_id}\")\n\n\nreq = st.text_area(\n   \"Describe your trip request\",\n   value=st.session_state.get(\"user_request\", \"Plan a 5-day trip from Dubai to Istanbul in April. Budget $1800. Prefer museums, street food, and a relaxed pace.\"),\n   height=120\n)\nst.session_state.user_request = req\n\n\ncolA, colB = st.columns([1,1])\nrun_plan = colA.button(\"1) Generate Plan (LLM)\")\nresume_btn = colB.button(\"2) Resume After Approval\")\n\n\nif run_plan:\n   st.session_state.ran_once = True\n   st.session_state.interrupt_payload = None\n   st.session_state.last_execution = None\n\n\n   initial = {\"user_request\": req, \"plan\": {}, \"approval\": {}, \"execution\": {}}\n   out = graph.invoke(initial, config=config)\n\n\n   if \"__interrupt__\" in out and out[\"__interrupt__\"]:\n       st.session_state.interrupt_payload = out[\"__interrupt__\"][0].value\n   else:\n       st.session_state.last_execution = out.get(\"execution\")\n\n\npayload = st.session_state.get(\"interrupt_payload\")\n\n\nif payload:\n   st.subheader(\"Plan proposed by agent (editable)\")\n   plan = payload.get(\"plan\", {})\n   left, right = st.columns([1,1])\n\n\n   with left:\n       st.write(\"**Edit JSON (advanced):**\")\n       edited_text = st.text_area(\"Plan JSON\", value=json.dumps(plan, indent=2), height=420)\n\n\n   with right:\n       st.write(\"**Quick actions:**\")\n       approved = st.radio(\"Decision\", options=[\"Approve\", \"Reject\"], index=0)\n       st.write(\"Tip: If you edit JSON, keep it valid. You can also reject and re-run planning.\")\n\n\n   try:\n       edited_plan = json.loads(edited_text)\n       json_ok = True\n   except Exception as e:\n       json_ok = False\n       st.error(f\"Invalid JSON: {e}\")\n\n\n   if resume_btn:\n       if not json_ok:\n           st.stop()\n\n\n       decision = {\n           \"approved\": (approved == \"Approve\"),\n           \"edited_plan\": edited_plan\n       }\n       out2 = graph.invoke(Command(resume=decision), config=config)\n       st.session_state.interrupt_payload = None\n       st.session_state.last_execution = out2.get(\"execution\")\n\n\nexec_result = st.session_state.get(\"last_execution\")\nif exec_result:\n   st.subheader(\"Execution result\")\n   st.json(exec_result)\n   if exec_result.get(\"status\") == \"executed\":\n       st.success(\"Tools executed only AFTER approval <img decoding=\"async\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/2705.png\" alt=\"\u2705\" class=\"wp-smiley\" \/>\")\n   else:\n       st.warning(\"Not executed (rejected or not approved).\")\n'''\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p>We construct the LangGraph workflow by separating planning, approval, and execution into distinct nodes. We deliberately interrupt the graph after planning so we can review and control the agent\u2019s intent. We only allow tool execution to proceed when explicit human approval is provided.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\" no-line-numbers\"><code class=\" no-wrap language-php\">import pathlib\npathlib.Path(\"app.py\").write_text(app_code)\n\n\n!streamlit run app.py --server.port 8501 --server.address 0.0.0.0 &amp; sleep 2\n!lt --port 8501<\/code><\/pre>\n<\/div>\n<\/div>\n<p>We connect the agent workflow to a live Streamlit interface that supports editing, approval, and rejection of plans. We persist the state across runs using a thread identifier so the agent behaves consistently across interactions. We finally launch the app and make it publicly available, enabling real human-in-the-loop collaboration.<\/p>\n<p>In conclusion, we demonstrated how plan-and-execute agents become significantly more reliable when humans remain in the loop at the right moment. We showed that interrupts are not just a technical feature but a design primitive for building trust, accountability, and collaboration into agent systems. By separating planning from execution and inserting a clear approval boundary, we ensured that tools run only with human consent and context. This pattern scales beyond travel planning to any high-stakes automation, giving us agents that think with us rather than act for us.<\/p>\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n<p>Check out the\u00a0<strong><a href=\"https:\/\/github.com\/Marktechpost\/AI-Tutorial-Codes-Included\/blob\/main\/AI%20Agents%20Codes\/human_in_the_loop_plan_execute_agent_langgraph_streamlit_Marktechpost.ipynb\" target=\"_blank\" rel=\"noreferrer noopener\">Full Codes here<\/a>.\u00a0<\/strong>Also,\u00a0feel free to follow us on\u00a0<strong><a href=\"https:\/\/x.com\/intent\/follow?screen_name=marktechpost\" target=\"_blank\" rel=\"noreferrer noopener\"><mark>Twitter<\/mark><\/a><\/strong>\u00a0and don\u2019t forget to join our\u00a0<strong><a href=\"https:\/\/www.reddit.com\/r\/machinelearningnews\/\" target=\"_blank\" rel=\"noreferrer noopener\">100k+ ML SubReddit<\/a><\/strong>\u00a0and Subscribe to\u00a0<strong><a href=\"https:\/\/www.aidevsignals.com\/\" target=\"_blank\" rel=\"noreferrer noopener\">our Newsletter<\/a><\/strong>. Wait! are you on telegram?\u00a0<strong><a href=\"https:\/\/t.me\/machinelearningresearchnews\" target=\"_blank\" rel=\"noreferrer noopener\">now you can join us on telegram as well.<\/a><\/strong><\/p>\n<p>The post <a href=\"https:\/\/www.marktechpost.com\/2026\/02\/16\/how-to-build-human-in-the-loop-plan-and-execute-ai-agents-with-explicit-user-approval-using-langgraph-and-streamlit\/\">How to Build Human-in-the-Loop Plan-and-Execute AI Agents with Explicit User Approval Using LangGraph and Streamlit<\/a> appeared first on <a href=\"https:\/\/www.marktechpost.com\/\">MarkTechPost<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>In this tutorial, we build a h&hellip;<\/p>\n","protected":false},"author":1,"featured_media":29,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-424","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=\/wp\/v2\/posts\/424","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=424"}],"version-history":[{"count":0,"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=\/wp\/v2\/posts\/424\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=\/wp\/v2\/media\/29"}],"wp:attachment":[{"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=424"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=424"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=424"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}