Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
79d452d
refactor: streamline flight search agent by removing unused imports a…
teetangh Sep 15, 2025
2271041
Merge branch 'main' into fix/tool-wrapping
teetangh Sep 17, 2025
9dcd9da
refactor: simplify LlamaIndex agent creation by removing unused param…
teetangh Sep 17, 2025
36e1c58
feat: enhance flight search agent with Capella AI compatibility
teetangh Sep 18, 2025
dec87ab
refactor: improve flight search agent prompt handling and error logging
teetangh Sep 18, 2025
cf59eaa
refactor: clean up logging and debug statements in flight search agent
teetangh Sep 18, 2025
5bcae2c
refactor: streamline flight search agent by removing custom tool hand…
teetangh Sep 18, 2025
e74000e
refactor: enhance flight search agent with improved tool integration …
teetangh Sep 18, 2025
93bbf9f
refactor: enhance flight search agent with improved tool handling and…
teetangh Sep 23, 2025
50e1981
refactor: enhance flight search agent with structured output and impr…
teetangh Sep 23, 2025
ec31654
refactor: enhance flight search agent with improved logging and respo…
teetangh Sep 23, 2025
07198b3
refactor: enhance flight search agent with tool result extraction and…
teetangh Sep 23, 2025
215cc81
refactor: improve logging and content handling in flight search agent
teetangh Sep 23, 2025
952bdfd
feat: add new flight search assistant features and improve tool handling
teetangh Sep 24, 2025
50a1d46
fix: update flight booking details and enhance input validation
teetangh Sep 24, 2025
d1b233a
deleted logs
teetangh Sep 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,282 changes: 1,282 additions & 0 deletions docs/AGENTC_PACKAGE_REFERENCE.md

Large diffs are not rendered by default.

Empty file added docs/main-updates.md
Empty file.
347 changes: 347 additions & 0 deletions notebooks/flight_search_agent_langraph/architecture.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from couchbase.options import ClusterOptions
import dotenv
import pandas as pd
from langchain_couchbase.vectorstores import CouchbaseVectorStore
from langchain_couchbase.vectorstores import CouchbaseSearchVectorStore
from tqdm import tqdm

# Import kagglehub only when needed to avoid import errors during indexing
Expand Down Expand Up @@ -153,7 +153,7 @@ def load_to_vector_store(
review_texts = self.process_to_texts()

# Setup vector store for the target collection
vector_store = CouchbaseVectorStore(
vector_store = CouchbaseSearchVectorStore(
cluster=cluster,
bucket_name=bucket_name,
scope_name=scope_name,
Expand Down
32 changes: 16 additions & 16 deletions notebooks/flight_search_agent_langraph/data/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
# Query 2: Flight booking LAX to JFK for tomorrow, 2 passengers, business class
"""Flight Booking Confirmed!

Booking ID: FL08061563CACD
Booking ID: FL09251563CACD
Route: LAX → JFK
Departure Date: 2025-08-06
Departure Date: 2025-09-25
Passengers: 2
Class: business
Total Price: $1500.00
Expand All @@ -45,9 +45,9 @@
# Query 3: Flight booking JFK to MIA for next week
"""Flight Booking Confirmed!

Booking ID: FL08124E7B9C2A
Booking ID: FL10014E7B9C2A
Route: JFK → MIA
Departure Date: 2025-08-12
Departure Date: 2025-10-01
Passengers: 1
Class: economy
Total Price: $250.00
Expand All @@ -63,42 +63,42 @@
"""Your Current Bookings (2 found):

Booking 1:
Booking ID: FL08061563CACD
Booking ID: FL09251563CACD
Route: LAX → JFK
Date: 2025-08-06
Date: 2025-09-25
Passengers: 2
Class: business
Total: $1500.00
Status: confirmed
Booked: 2025-08-05
Booked: 2025-09-24

Booking 2:
Booking ID: FL08124E7B9C2A
Booking ID: FL10014E7B9C2A
Route: JFK → MIA
Date: 2025-08-12
Date: 2025-10-01
Passengers: 1
Class: economy
Total: $250.00
Status: confirmed
Booked: 2025-08-05""",
Booked: 2025-09-24""",

# Query 5: SpiceJet service quality reviews
"""Found 5 relevant airline reviews for 'SpiceJet service':
"""Found 5 relevant airline reviews for 'SpiceJet service quality':

Review 1:
Airline: SpiceJet. Title: "Service is impeccable". Review: ✅ Trip Verified | Much better than airbus models. Even the basic economy class has ambient lighting. Better personal air vents and better spotlights. Even overhead storage bins are good. Service is impeccable with proper care taken of guests...
Airline: SpiceJet. Title: "Great travel experience". Review: ✅ Trip Verified | Marvelous courteous crew who took good care of all passengers. They should be rewarded for the patience shown towards the unruly ones. Great travel experience.. Rating: 10.0/10. Reviewer: Ranjita Pandey. Date: 18th April 2024. Recommended: yes

Review 2:
Airline: SpiceJet. Title: "good service by the crew". Review: ✅ Trip Verified | I have had good service by the crew. It was amazing, the crew was very enthusiastic and warm welcome. It was one of the best services in my experience.. Rating: 10.0/10. Reviewer: K Mansour. Date: 10th August 2024. Recom...
Airline: SpiceJet. Title: "good service by the crew". Review: ✅ Trip Verified | I have had good service by the crew. It was amazing, the crew was very enthusiastic and warm welcome. It was one of the best services in my experience.. Rating: 10.0/10. Reviewer: K Mansour. Date: 10th August 2024. Recommended: yes

Review 3:
Airline: SpiceJet. Title: "outstanding service I experienced". Review: Not Verified | I wanted to take a moment to express my sincere thanks for the outstanding service I experienced on my recent flight from Pune to Delhi. SG-8937. From the moment I boarded, the warmth and friendliness of the air h...
Airline: SpiceJet. Title: "delayed both ways by many hours". Review: Not Verified | Flight was delayed both ways by many hours. Poor service for the same price as other airlines like IndiGo. No wifi or other amenities to compensate for terrible service.. Rating: 2.0/10. Reviewer: Somil Jain Jain. Date: 20th May 2022. Recommended: no

Review 4:
Airline: SpiceJet. Title: "efficient and warm onboard service". Review: ✅ Trip Verified | New Delhi to Kolkata. Delighted with the prompt, efficient and warm onboard service provided by the crew. Appreciate their efforts towards customer centricity.. Rating: 10.0/10. Reviewer: Debashis Roy. Date: 2...
Airline: SpiceJet. Title: "Excellent service". Review: ✅ Trip Verified | Excellent service by the ground staff courteous beyond expectations always willing to help in the real sense and not lipservice i will recommend to all whom I know. Rating: 10.0/10. Reviewer: Ramanathan Ramchandra. Date: 1st November 2023. Recommended: yes

Review 5:
Airline: SpiceJet. Title: "Service is very good". Review: Service is very good, I am impressed with Miss Renu who gave the best services ever. Thanks to Renu who is very sweet by her nature as well as her service. Rating: 9.0/10. Reviewer: Sanjay Patnaik. Date: 21st September 2023. Recommended: ye...""",
Airline: SpiceJet. Title: "hospitality service given". Review: ✅ Trip Verified | Seats are comparable if compare to other budget airlines. Cabin crews are friendly and hospitality service given with smiles. Very happy and enjoy experience.. Rating: 8.0/10. Reviewer: A Bameen. Date: 20th March 2022. Recommended: yes""",
]

# Create dictionary for backward compatibility
Expand Down
256 changes: 51 additions & 205 deletions notebooks/flight_search_agent_langraph/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@
from couchbase.cluster import Cluster
from couchbase.exceptions import KeyspaceNotFoundException
from couchbase.options import ClusterOptions
from langchain.agents import AgentExecutor, create_react_agent
from langchain_core.prompts import PromptTemplate
from langchain_core.tools import Tool
from pydantic import SecretStr


Expand Down Expand Up @@ -70,6 +67,9 @@ def find_project_root():
dotenv.load_dotenv(override=True)


# Agent Catalog tool integration - tools will be imported from tools/ directory


class FlightSearchState(agentc_langgraph.agent.State):
"""State for flight search conversations - single user system."""

Expand All @@ -93,223 +93,69 @@ def __init__(self, catalog: agentc.Catalog, span: agentc.Span, chat_model=None):
chat_model=chat_model, catalog=catalog, span=span, prompt_name="flight_search_assistant"
)

def _get_tool_names(self) -> set[str]:
"""Get available tool names from agent metadata."""
if hasattr(self, 'tools') and self.tools:
return {tool.name for tool in self.tools}
return set()

def _extract_tool_results(self, messages):
"""Extract tool results from messages for production display."""
# Get available tool names dynamically
tool_names = self._get_tool_names()

# Find the first successful ToolMessage (skip error results)
for message in messages:
if (hasattr(message, 'name') and
message.name in tool_names and
not message.content.startswith("Error:")):
return message.content

return None

def _invoke(
self,
span: agentc.Span,
state: FlightSearchState,
config: langchain_core.runnables.RunnableConfig,
) -> FlightSearchState:
"""Handle flight search conversation using ReActAgent."""
"""Handle flight search conversation using proper Agent Catalog patterns."""

# Initialize conversation if this is the first message
if not state["messages"]:
initial_msg = langchain_core.messages.HumanMessage(content=state["query"])
state["messages"].append(initial_msg)
logger.info(f"Flight Query: {state['query']}")

# Get prompt resource first - we'll need it for the ReAct agent
prompt_resource = self.catalog.find("prompt", name="flight_search_assistant")

# Get tools from Agent Catalog with simplified discovery
tools = []
tool_names = [
"lookup_flight_info",
"save_flight_booking",
"retrieve_flight_bookings",
"search_airline_reviews",
]

for tool_name in tool_names:
try:
# Find tool using Agent Catalog
catalog_tool = self.catalog.find("tool", name=tool_name)
if catalog_tool:
logger.info(f"✅ Found tool: {tool_name}")
else:
logger.error(f"❌ Tool not found: {tool_name}")
continue

except Exception as e:
logger.error(f"❌ Failed to find tool {tool_name}: {e}")
continue

# Create wrapper function to handle proper parameter parsing
def create_tool_wrapper(original_tool, name):
"""Create a wrapper for Agent Catalog tools with robust input handling."""

def wrapper_func(tool_input: str) -> str:
"""Wrapper function that handles input parsing and error handling."""
try:
logger.info(f"🔧 Tool {name} called with raw input: {repr(tool_input)}")

# Robust input sanitization to handle ReAct format artifacts
if isinstance(tool_input, str):
# Remove ReAct format artifacts that get mixed into input
clean_input = tool_input.strip()

# Remove common ReAct artifacts
artifacts_to_remove = [
'\nObservation', 'Observation', '\nThought:', 'Thought:',
'\nAction:', 'Action:', '\nAction Input:', 'Action Input:',
'\nFinal Answer:', 'Final Answer:'
]

for artifact in artifacts_to_remove:
if artifact in clean_input:
clean_input = clean_input.split(artifact)[0]

# Clean up quotes and whitespace
clean_input = clean_input.strip().strip("\"'").strip()
# Normalize whitespace
clean_input = " ".join(clean_input.split())

tool_input = clean_input

logger.info(f"🧹 Tool {name} cleaned input: {repr(tool_input)}")

# Call appropriate tool with proper parameter handling
if name == "lookup_flight_info":
# Parse airport codes from input
import re

source = None
dest = None

# 1) Support key=value style inputs from ReAct (e.g., source_airport="JFK", destination_airport="LAX")
try:
m_src = re.search(r"source_airport\s*[:=]\s*\"?([A-Za-z]{3})\"?", tool_input, re.I)
m_dst = re.search(r"destination_airport\s*[:=]\s*\"?([A-Za-z]{3})\"?", tool_input, re.I)
if m_src and m_dst:
source = m_src.group(1).upper()
dest = m_dst.group(1).upper()
except Exception:
pass

# 2) Fallback: comma separated codes (e.g., "JFK,LAX")
if source is None or dest is None:
if ',' in tool_input:
parts = tool_input.split(',')
if len(parts) >= 2:
source = parts[0].strip().upper()
dest = parts[1].strip().upper()

# 3) Fallback: natural language (e.g., "JFK to LAX")
if source is None or dest is None:
words = tool_input.upper().split()
airport_codes = [w for w in words if len(w) == 3 and w.isalpha()]
if len(airport_codes) >= 2:
source, dest = airport_codes[0], airport_codes[1]

if not source or not dest:
return "Error: Please provide source and destination airports (e.g., JFK,LAX or JFK to LAX)"

result = original_tool.func(source_airport=source, destination_airport=dest)

elif name == "save_flight_booking":
result = original_tool.func(booking_input=tool_input)

elif name == "retrieve_flight_bookings":
# Handle empty input for "all bookings"
if not tool_input or tool_input.lower() in ["", "all", "none"]:
result = original_tool.func(booking_query="")
else:
result = original_tool.func(booking_query=tool_input)

elif name == "search_airline_reviews":
if not tool_input:
return "Error: Please provide a search query for airline reviews"
result = original_tool.func(query=tool_input)

else:
# Generic fallback - pass as first positional argument
result = original_tool.func(tool_input)

logger.info(f"✅ Tool {name} executed successfully")
return str(result) if result is not None else "No results found"

except Exception as e:
error_msg = f"Error in tool {name}: {str(e)}"
logger.error(f"❌ {error_msg}")
return error_msg

return wrapper_func

# Create LangChain tool with descriptive information
tool_descriptions = {
"lookup_flight_info": "Find available flights between airports. Input: 'JFK,LAX' or 'JFK to LAX'. Returns flight options with airlines and aircraft.",
"save_flight_booking": "Create a flight booking. Input: 'JFK,LAX,2025-12-25' or natural language. Handles passenger count and class automatically.",
"retrieve_flight_bookings": "View existing bookings. Input: empty string for all bookings, or 'JFK,LAX,2025-12-25' for specific booking.",
"search_airline_reviews": "Search airline customer reviews. Input: 'SpiceJet service' or 'food quality'. Returns passenger reviews and ratings."
}

langchain_tool = Tool(
name=tool_name,
description=tool_descriptions.get(tool_name, f"Tool for {tool_name.replace('_', ' ')}"),
func=create_tool_wrapper(catalog_tool, tool_name),
)
tools.append(langchain_tool)

# Use the Agent Catalog prompt content directly - get first result if it's a list
if isinstance(prompt_resource, list):
prompt_resource = prompt_resource[0]

# Safely get the content from the prompt resource
prompt_content = getattr(prompt_resource, "content", "")
if not prompt_content:
prompt_content = "You are a helpful flight search assistant. Use the available tools to help users with their flight queries."

# Inject current date into the prompt content
import datetime

current_date = datetime.date.today().strftime("%Y-%m-%d")
prompt_content = prompt_content.replace("{current_date}", current_date)

# Use the Agent Catalog prompt content directly - it already has ReAct format
react_prompt = PromptTemplate.from_template(str(prompt_content))

# Create ReAct agent with tools and prompt
agent = create_react_agent(self.chat_model, tools, react_prompt)

# Custom parsing error handler - force stopping on parsing errors
def handle_parsing_errors(error):
"""Custom handler for parsing errors - force early termination."""
error_msg = str(error)
if "both a final answer and a parse-able action" in error_msg:
# Force early termination - return a reasonable response
return "Final Answer: I encountered a parsing error. Please reformulate your request."
elif "Missing 'Action:'" in error_msg:
return "I need to use the correct format with Action: and Action Input:"
# Use Agent Catalog's built-in create_react_agent method (like FastAPI example)
agent = self.create_react_agent(span)

# Execute the agent with proper Agent Catalog integration
response = agent.invoke(input=state, config=config)
logger.info(f"🔍FULL Agent response: {response}")

# Extract tool results instead of conversational responses for production display
if "messages" in response and response["messages"]:
# Find the first successful tool result
tool_content = self._extract_tool_results(response["messages"])
if tool_content:
# Use tool results for production display
assistant_msg = langchain_core.messages.AIMessage(content=tool_content)
state["messages"].append(assistant_msg)
logger.info(f"📊 Tool results: {tool_content}")
else:
return f"Final Answer: I encountered an error processing your request. Please try again."

# Create agent executor - very strict: only 2 iterations max
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True,
handle_parsing_errors=handle_parsing_errors,
max_iterations=2, # STRICT: 1 tool call + 1 Final Answer only
early_stopping_method="force", # Force stop
return_intermediate_steps=True,
)
# Fallback to last AI message if no tool results
last_message = response["messages"][-1]
state["messages"].append(last_message)
logger.info(f"📊 Agent response: {last_message.content}...")
else:
# Fallback to output field
output_content = response.get("output", "No response generated")
assistant_msg = langchain_core.messages.AIMessage(content=output_content)
state["messages"].append(assistant_msg)
logger.info(f"📊 Agent output: {output_content}...")

# Execute the agent
response = agent_executor.invoke({"input": state["query"]})

# Extract tool outputs from intermediate_steps and store in search_results
if "intermediate_steps" in response and response["intermediate_steps"]:
tool_outputs = []
for step in response["intermediate_steps"]:
if isinstance(step, tuple) and len(step) >= 2:
# step[0] is the action, step[1] is the tool output/observation
tool_output = str(step[1])
if tool_output and tool_output.strip():
tool_outputs.append(tool_output)
state["search_results"] = tool_outputs

# Add response to conversation
assistant_msg = langchain_core.messages.AIMessage(content=response["output"])
state["messages"].append(assistant_msg)
# Mark as resolved
state["resolved"] = True

return state
Expand Down
Loading