Skip to content

Try to get web search working#4

Closed
tpaulshippy wants to merge 1 commit intomainfrom
feature/web-search-tavily
Closed

Try to get web search working#4
tpaulshippy wants to merge 1 commit intomainfrom
feature/web-search-tavily

Conversation

@tpaulshippy
Copy link
Copy Markdown
Owner

@tpaulshippy tpaulshippy commented Apr 4, 2026

Summary by CodeRabbit

  • Refactor
    • Improved AI-powered search functionality with enhanced tool execution handling and refined iteration controls. The system now features more robust response extraction to reliably handle various response formats and edge cases, ensuring more consistent performance.
    • Optimized backend integration testing through improved import initialization sequencing.

@tpaulshippy tpaulshippy requested a review from Copilot April 4, 2026 06:13
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

📝 Walkthrough

Walkthrough

The PR refactors the chat model's tool execution from LangChain's create_agent() to manual bind_tools() with a bounded iteration loop (max 5). Message handling is now explicitly constructed and tool responses are manually appended as ToolMessage objects. Response extraction scans accumulated messages to find the last non-tool-calling AIMessage. A test file's imports are reordered to execute after Django setup.

Changes

Cohort / File(s) Summary
Tool Binding & Response Loop Refactor
back/bots/models/chat.py
Replaced LangChain create_agent() with bind_tools() and manual invocation loop. Explicit SystemMessage/HumanMessage construction, bounded iterations, and tool-call handling via ToolMessage. Reworked response extraction to scan message history for final non-tool AIMessage.
Import Initialization Order
back/test_integration.py
Moved requests, User, RefreshToken, and Bot/AiModel imports to execute after django.setup() call.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 No more agents to summon with a wave,
We bind the tools ourselves and misbehave!
Five loops max, messages in a line,
Each tool call answered, response extraction divine—
The rabbit rejoices at this refactor so fine! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Try to get web search working' is vague and does not clearly describe the specific changes made. It uses generic phrasing that doesn't convey meaningful information about the implementation details (refactoring from LangChain to manual tool binding). Consider a more specific title like 'Refactor web search to use bind_tools instead of LangChain create_agent' or 'Replace LangChain agent with manual tool invocation loop' to better communicate the core change.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/web-search-tavily

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR attempts to enable/repair web search tool-calling by replacing the previous LangChain agent construction with an explicit tool-calling loop over ChatBedrock.bind_tools(...), and adjusts an integration script to import Django-dependent modules only after django.setup().

Changes:

  • Migrate web-search execution from create_agent(...) to a manual tool-call loop using bind_tools + ToolMessage.
  • Add ToolMessage handling and new logic for extracting the final AI response from the message sequence.
  • Reorder imports in back/test_integration.py to occur after Django initialization.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
back/bots/models/chat.py Reworks the web-search “agent” execution to use Bedrock tool binding and a manual tool loop.
back/test_integration.py Moves Django-dependent imports to after django.setup() for safer script execution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +134 to +149
# Agentagent loop - keep invoking until no more tool calls
max_iterations = 5
iteration = 0

while iteration < max_iterations:
iteration += 1
logger.info(f"🤖 AGENT_LOOP_ITERATION: {iteration}")

# Call the model
response = model_with_tools.invoke(messages)
messages.append(response)

# Check if model wants to call a tool
if not response.tool_calls:
logger.info(f"🤖 AGENT_LOOP_COMPLETE: no more tool calls after {iteration} iterations")
break
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manual tool loop stops after max_iterations, but if the model is still returning tool calls at that point there may be no final AIMessage without tool_calls. In that case response_text falls back to the last AIMessage which is likely a tool-call message (often empty), resulting in a blank or incorrect assistant reply. Handle the max-iteration exit explicitly (e.g., raise/log an error and return a safe message, or force one final model call after the last ToolMessage) so callers always get a real final response.

Copilot uses AI. Check for mistakes.
# Build and run the agent loop manually
messages = [SystemMessage(content=self.get_system_message()), HumanMessage(content=agent_input)]

# Agentagent loop - keep invoking until no more tool calls
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: # Agentagent loop should be corrected (e.g., # Agent loop).

Suggested change
# Agentagent loop - keep invoking until no more tool calls
# Agent loop - keep invoking until no more tool calls

Copilot uses AI. Check for mistakes.
Comment on lines 4 to 8
from langchain_aws import ChatBedrock
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, ToolMessage
from langchain_core.tools import tool
from langchain_core.callbacks.base import BaseCallbackHandler
from langchain.agents import create_agent
from tavily import TavilyClient
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create_agent is removed in this PR, but the existing unit test back/bots/tests/test_chat.py patches bots.models.chat.create_agent. With this change, that test will start failing (patch target no longer exists). Update the tests to patch/mock the new bind_tools(...).invoke(...) flow instead.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
back/bots/models/chat.py (1)

118-211: Extract the tool orchestration into helper methods.

This branch now combines model setup, tool execution, loop control, response parsing, and persistence in one place. Pulling those pieces apart would make the web-search path much easier to test and reason about.

As per coding guidelines, back/bots/**/*.py: "Keep methods focused and concise, under 10 lines when possible, extracting complex logic into separate methods".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@back/bots/models/chat.py` around lines 118 - 211, The current method mixes
model setup, loop control, tool execution and response parsing; split it into
focused helpers: implement _create_chat_with_tools(self, web_search) to
construct ChatBedrock(model_id=...) and call bind_tools, _run_agent_loop(self,
model_with_tools, messages, max_iterations=5) to perform the while loop and
return the final messages list, _process_tool_call(self, tool_call, web_search)
to execute a single tool call (handling "web_search" via web_search.invoke and
returning a ToolMessage), and _extract_final_response(self, messages) to locate
the last AIMessage without tool_calls and return (response_text,
usage_metadata); update the main flow to call _extract_agent_input,
_create_chat_with_tools, build initial messages, call _run_agent_loop which uses
_process_tool_call internally, then call _extract_final_response to populate
response_text and usage_metadata.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@back/bots/models/chat.py`:
- Around line 143-144: The code calls model_with_tools.invoke(messages) which
may append multiple AIMessage responses but only captures usage_metadata from
the last AIMessage, causing under-reported token usage; update the handling
after each invoke to accumulate token counts from every AIMessage's
usage_metadata (e.g., sum input_tokens/output_tokens from each AIMessage in
messages or from the returned response objects) after each call to
model_with_tools.invoke and before appending additional messages so that total
input/output/token usage reflects all tool-enabled invocations; apply the same
accumulation fix where model_with_tools.invoke is used (including the block
around lines 176-197) and ensure you update whatever counters or usage fields
(input_tokens, output_tokens, total_tokens, usage_metadata) used by
billing/quota logic.
- Around line 118-123: The branch that enables tools incorrectly constructs a
new ChatBedrock instance (ChatBedrock(model_id=...)) instead of reusing the
injected client self.ai, causing loss of injected callbacks and differing
behavior; change the code to use self.ai (which already implements bind_tools)
when creating model_with_tools (e.g., call self.ai.bind_tools([web_search]) or
equivalent) so the tool-enabled path reuses the same client and preserves the
seams used by get_response_standard and tests.
- Around line 135-149: The loop can exit on max_iterations while the last
response still contains a tool_call stub, so after the while using
max_iterations/iteration and the last response from model_with_tools.invoke,
detect if iteration >= max_iterations and response.tool_calls is truthy; in that
case produce and append a proper terminal assistant message (e.g., an AIMessage
summarizing that the agent could not finish the task or asking to try again) or
re-invoke model_with_tools.invoke with instructions to return a final assistant
answer (no tool_calls) before persisting — update the code paths around the loop
and the fallback persistence (the messages list, response variable, and the code
that saves the last AIMessage) to ensure a non-tool_call terminal assistant
message is saved and returned.
- Around line 125-133: The code currently discards prior turns by building
messages as only SystemMessage + last HumanMessage (using agent_input); instead,
preserve the full prior conversation/multimodal context by using the original
message list (or the output of the existing get_input/_extract_agent_input flow)
when creating the agent messages: convert message_list (or the previously
assembled messages from get_input) into LLM message objects, prepend the
SystemMessage from get_system_message(), and then append the new
HumanMessage(content=agent_input) rather than replacing history; update the code
around agent_input, _extract_agent_input, get_system_message, and the messages
list construction so prior turns remain when web search is enabled.

---

Nitpick comments:
In `@back/bots/models/chat.py`:
- Around line 118-211: The current method mixes model setup, loop control, tool
execution and response parsing; split it into focused helpers: implement
_create_chat_with_tools(self, web_search) to construct ChatBedrock(model_id=...)
and call bind_tools, _run_agent_loop(self, model_with_tools, messages,
max_iterations=5) to perform the while loop and return the final messages list,
_process_tool_call(self, tool_call, web_search) to execute a single tool call
(handling "web_search" via web_search.invoke and returning a ToolMessage), and
_extract_final_response(self, messages) to locate the last AIMessage without
tool_calls and return (response_text, usage_metadata); update the main flow to
call _extract_agent_input, _create_chat_with_tools, build initial messages, call
_run_agent_loop which uses _process_tool_call internally, then call
_extract_final_response to populate response_text and usage_metadata.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 62382766-50a0-4fee-9fa1-88105f5223a6

📥 Commits

Reviewing files that changed from the base of the PR and between 0866c85 and 550bcf7.

📒 Files selected for processing (2)
  • back/bots/models/chat.py
  • back/test_integration.py

Comment on lines +118 to +123
# Create chat model with tool binding
chat_model = ChatBedrock(model_id=self.ai.model_id)
tools = [web_search]

# Create modern agent with tool calling support
# This is the recommended approach per LangChain docs
agent = create_agent(
model=chat_model,
tools=tools,
system_prompt=self.get_system_message(),
debug=settings.DEBUG
)
# Bind tools to the model for proper tool calling
model_with_tools = chat_model.bind_tools(tools)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't bypass ai in the tool-enabled path.

Line 119 creates a new ChatBedrock instead of reusing self.ai, so this branch ignores the injected client/callbacks used by get_response_standard(). That makes tool-enabled chats behave differently from the non-tool path and leaves this code path without the same seam the current tests were mocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@back/bots/models/chat.py` around lines 118 - 123, The branch that enables
tools incorrectly constructs a new ChatBedrock instance
(ChatBedrock(model_id=...)) instead of reusing the injected client self.ai,
causing loss of injected callbacks and differing behavior; change the code to
use self.ai (which already implements bind_tools) when creating model_with_tools
(e.g., call self.ai.bind_tools([web_search]) or equivalent) so the tool-enabled
path reuses the same client and preserves the seams used by
get_response_standard and tests.

Comment on lines 125 to +133
# Extract text input from message_list for agent
agent_input = self._extract_agent_input(message_list)

logger.info(f"Invoking agent with input: {agent_input[:100]}...")
logger.info("🤖 AGENT_INVOKE_START: web_search tool available")

# Invoke agent - the CompiledStateGraph handles tool loop internally
response = agent.invoke({"messages": [HumanMessage(content=agent_input)]})
# Build and run the agent loop manually
messages = [SystemMessage(content=self.get_system_message()), HumanMessage(content=agent_input)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preserve prior turns when web search is enabled.

Lines 125-132 throw away the history that get_input() just assembled and restart the model with only SystemMessage + last HumanMessage. Any follow-up question, and any multimodal context already present in message_list, is lost as soon as enable_web_search is on.

🧩 Minimal fix
-            # Build and run the agent loop manually
-            messages = [SystemMessage(content=self.get_system_message()), HumanMessage(content=agent_input)]
+            # Build and run the agent loop manually with the existing chat history
+            messages = list(message_list)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@back/bots/models/chat.py` around lines 125 - 133, The code currently discards
prior turns by building messages as only SystemMessage + last HumanMessage
(using agent_input); instead, preserve the full prior conversation/multimodal
context by using the original message list (or the output of the existing
get_input/_extract_agent_input flow) when creating the agent messages: convert
message_list (or the previously assembled messages from get_input) into LLM
message objects, prepend the SystemMessage from get_system_message(), and then
append the new HumanMessage(content=agent_input) rather than replacing history;
update the code around agent_input, _extract_agent_input, get_system_message,
and the messages list construction so prior turns remain when web search is
enabled.

Comment on lines +135 to +149
max_iterations = 5
iteration = 0

while iteration < max_iterations:
iteration += 1
logger.info(f"🤖 AGENT_LOOP_ITERATION: {iteration}")

# Call the model
response = model_with_tools.invoke(messages)
messages.append(response)

# Check if model wants to call a tool
if not response.tool_calls:
logger.info(f"🤖 AGENT_LOOP_COMPLETE: no more tool calls after {iteration} iterations")
break
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle loop exhaustion explicitly.

If the model is still returning tool_calls on the fifth pass, the loop exits without a terminal assistant answer. The fallback at Lines 202-211 then persists the last AIMessage, which in that case is usually an empty/partial tool-call stub, and back/bots/views/get_chat_response.py:82-83 returns that straight to the client.

🛑 Safer fallback
             max_iterations = 5
             iteration = 0
+            exhausted = True

             while iteration < max_iterations:
                 iteration += 1
                 logger.info(f"🤖 AGENT_LOOP_ITERATION: {iteration}")
@@
                 # Check if model wants to call a tool
                 if not response.tool_calls:
+                    exhausted = False
                     logger.info(f"🤖 AGENT_LOOP_COMPLETE: no more tool calls after {iteration} iterations")
                     break
@@
-            if not response_text:
+            if exhausted:
+                logger.error("🤖 AGENT_LOOP_ABORTED: max iterations reached without a final response")
+                response_text = "I'm sorry, I couldn't complete the web search. Please try again."
+            elif not response_text:
                 # Fallback: get the last message
                 for msg in reversed(messages):
                     if isinstance(msg, AIMessage):

Also applies to: 202-211

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@back/bots/models/chat.py` around lines 135 - 149, The loop can exit on
max_iterations while the last response still contains a tool_call stub, so after
the while using max_iterations/iteration and the last response from
model_with_tools.invoke, detect if iteration >= max_iterations and
response.tool_calls is truthy; in that case produce and append a proper terminal
assistant message (e.g., an AIMessage summarizing that the agent could not
finish the task or asking to try again) or re-invoke model_with_tools.invoke
with instructions to return a final assistant answer (no tool_calls) before
persisting — update the code paths around the loop and the fallback persistence
(the messages list, response variable, and the code that saves the last
AIMessage) to ensure a non-tool_call terminal assistant message is saved and
returned.

Comment on lines +143 to +144
response = model_with_tools.invoke(messages)
messages.append(response)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Accumulate token usage for every model invoke.

model_with_tools.invoke(messages) can run multiple times here, but usage_metadata is taken only from the last AIMessage. Tool-enabled chats will under-report tokens whenever a tool is used, which skews any quota/billing logic that reads input_tokens / output_tokens.

📊 Suggested fix
             response_text = ""
             usage_metadata = {"input_tokens": 0, "output_tokens": 0}
@@
                 # Call the model
                 response = model_with_tools.invoke(messages)
+                if getattr(response, "usage_metadata", None):
+                    usage_metadata["input_tokens"] += response.usage_metadata.get("input_tokens", 0)
+                    usage_metadata["output_tokens"] += response.usage_metadata.get("output_tokens", 0)
                 messages.append(response)
@@
-                    # Extract token usage
-                    if hasattr(msg, 'usage_metadata') and msg.usage_metadata:
-                        usage_metadata = msg.usage_metadata
-                    
                     logger.info(f"🤖 FINAL_RESPONSE: {len(response_text)} chars")
                     break

Also applies to: 176-197

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@back/bots/models/chat.py` around lines 143 - 144, The code calls
model_with_tools.invoke(messages) which may append multiple AIMessage responses
but only captures usage_metadata from the last AIMessage, causing under-reported
token usage; update the handling after each invoke to accumulate token counts
from every AIMessage's usage_metadata (e.g., sum input_tokens/output_tokens from
each AIMessage in messages or from the returned response objects) after each
call to model_with_tools.invoke and before appending additional messages so that
total input/output/token usage reflects all tool-enabled invocations; apply the
same accumulation fix where model_with_tools.invoke is used (including the block
around lines 176-197) and ensure you update whatever counters or usage fields
(input_tokens, output_tokens, total_tokens, usage_metadata) used by
billing/quota logic.

@tpaulshippy tpaulshippy closed this Apr 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants