Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8c5de70
fix: stop demo query streams from dying mid-flight (research#86)
galshubeli Aug 18, 2026
1c35208
fix(agents): pin the LLM retry budget so the timeout is a real ceiling
galshubeli Aug 18, 2026
bc50d89
test(e2e): off-topic query should show no SQL card at all
galshubeli Aug 18, 2026
779c7ec
fix: address PR #714 review feedback
galshubeli Aug 18, 2026
4d633cf
fix: offload the last two blocking calls in the query path (PR #714 r…
galshubeli Aug 19, 2026
81e3b40
fix(streaming): remove awaiting teardown; add DB timeouts and 3-stage…
galshubeli Aug 19, 2026
72dc7f5
fix(loaders,streaming): correct the DB timeout wiring and bound the q…
galshubeli Aug 19, 2026
07a8e59
fix(loaders): match a real statement_timeout directive, not the bare …
galshubeli Aug 19, 2026
cb835f6
Merge branch 'staging' into fix/demo-stream-failure-issue-86
galshubeli Aug 20, 2026
0d02574
fix: offload embeddings and schema loading; clamp URL timeout overrides
galshubeli Aug 20, 2026
5da0770
test: address lint-bot nits in the new offloading tests
galshubeli Aug 20, 2026
88fed2e
fix: stop orphaning speculative work, confine DB work to one worker, …
galshubeli Aug 20, 2026
8e318ff
test: add the timeout-validation suite that .gitignore silently dropped
galshubeli Aug 20, 2026
f565df7
test: explain the intentionally empty except in the cancellation test
galshubeli Aug 20, 2026
2188347
fix: offload the last three inline provider calls, and guard against …
galshubeli Aug 20, 2026
1ba6cad
fix: bound schema introspection, honour stricter timeout units, rejec…
galshubeli Aug 20, 2026
a14e3cc
fix(loaders): recognise PostgreSQL's long-option timeout directive form
galshubeli Aug 23, 2026
938f70c
fix: propagate producer cancellation, hold introspection slots, unbre…
galshubeli Aug 23, 2026
93bd93e
test: drop the side-effect import from the loader contract test
galshubeli Aug 23, 2026
a0a90c9
fix(agents): make LLM_TIMEOUT a total-call budget, not a per-attempt one
galshubeli Aug 23, 2026
b1e29c0
test: use one import style for api.config in the timeout tests
galshubeli Aug 23, 2026
c3004a1
fix(loaders): stop dropping URL options, bound socket reads, unbind t…
galshubeli Aug 23, 2026
b071f91
fix: drive retries against the remaining budget; enforce a real DB de…
galshubeli Aug 23, 2026
817ee0e
test: use one import style for the deadline module
galshubeli Aug 23, 2026
6d8dab3
fix: unblockable DB deadline, guard through commit, verdict-based LLM…
galshubeli Aug 23, 2026
676d512
fix: drop in-process PQcancel; honour the provider's Retry-After
galshubeli Aug 23, 2026
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
32 changes: 31 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,34 @@ FALKORDB_URL=redis://localhost:6379/0 # REQUIRED - change to your FalkorDB URL
# COMPLETION_MODEL=openai/gpt-4.1
# EMBEDDING_MODEL=openai/text-embedding-ada-002

# Wall-clock budget for one LLM call end to end, in seconds (default 90).
# This is the total, not per attempt: the per-attempt timeout handed to the
# provider is this divided by LLM_MAX_RETRIES + 1, so retries cannot push the
# real ceiling past it. A hung provider surfaces as an error within the budget
# instead of stalling the response stream.
# LLM_TIMEOUT=90
#
# Calls slower than this are logged at WARNING (default 20).
# LLM_SLOW_CALL_THRESHOLD=20
#
# Retry budget per LLM call (default 1). LLM_TIMEOUT applies per attempt, so
# this is pinned rather than left to the provider SDK and litellm defaults,
# which each retry and together multiply the effective ceiling.
# LLM_MAX_RETRIES=1
#
# Bounds for executing a user query against the target database. Offloading
# execution to a thread keeps the event loop free, but only these bound how
# long the query itself may run (a thread blocked in a socket read cannot be
# cancelled from Python). Seconds.
# DB_CONNECT_TIMEOUT=10
# DB_STATEMENT_TIMEOUT=60
#
# Schema introspection gets a larger deadline (it is metadata work over a whole
# database) and a cap on how many may occupy worker threads at once, since that
# executor is shared with every other offloaded call.
# DB_SCHEMA_TIMEOUT=300
# DB_SCHEMA_CONCURRENCY=2

# OpenAI - uses openai/gpt-4.1 and openai/text-embedding-ada-002
# OPENAI_API_KEY=your_openai_api_key

Expand All @@ -100,7 +128,9 @@ FALKORDB_URL=redis://localhost:6379/0 # REQUIRED - change to your FalkorDB URL
# Azure OpenAI (default fallback) - uses azure/gpt-4.1 and azure/text-embedding-ada-002
# AZURE_API_KEY=your_azure_api_key
# AZURE_API_BASE=https://your-resource.openai.azure.com/
# AZURE_API_VERSION=2023-05-15
# Must be 2025-03-01-preview or later — Graphiti memory writes use the
# Azure Responses API, which rejects older api-versions with HTTP 400.
# AZURE_API_VERSION=2025-03-01-preview

# -----------------------------
# OAuth configuration (optional — uncomment to enable login flows)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ docker run -p 5000:5000 -it \
-e FASTAPI_SECRET_KEY=your_secret_key \
-e AZURE_API_KEY=your_azure_api_key \
-e AZURE_API_BASE=https://your-resource.openai.azure.com/ \
-e AZURE_API_VERSION=2024-12-01-preview \
-e AZURE_API_VERSION=2025-03-01-preview \
falkordb/queryweaver
```

Expand Down
3 changes: 2 additions & 1 deletion api/agents/analysis_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ def get_analysis( # pylint: disable=too-many-arguments, too-many-positional-arg
self.messages.append({"role": "user", "content": prompt})

response = run_completion(
self.messages, self.custom_model, self.custom_api_key, temperature=0
self.messages, self.custom_model, self.custom_api_key,
label="analysis", temperature=0,
)
analysis = parse_response(response)
if isinstance(analysis["ambiguities"], list):
Expand Down
3 changes: 2 additions & 1 deletion api/agents/follow_up_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ def generate_follow_up_question(
try:
response = run_completion(
[{"role": "user", "content": prompt}],
self.custom_model, self.custom_api_key, temperature=0.9
self.custom_model, self.custom_api_key,
label="followup", temperature=0.9,
)
return response.strip()

Expand Down
12 changes: 4 additions & 8 deletions api/agents/healer_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@

import re
from typing import Dict, Callable, Any
from litellm import completion
from api.config import Config
from .utils import parse_response
from .utils import parse_response, run_completion


class HealerAgent:
Expand Down Expand Up @@ -224,14 +222,12 @@ def heal_and_execute( # pylint: disable=too-many-locals

for attempt in range(self.max_healing_attempts):
# Call LLM
response = completion(
model=Config.COMPLETION_MODEL,
messages=self.messages,
content = run_completion(
self.messages,
label=f"healer.attempt{attempt + 1}",
temperature=0.1,
max_tokens=2000
)

content = response.choices[0].message.content
self.messages.append({"role": "assistant", "content": content})

# Parse response
Expand Down
11 changes: 9 additions & 2 deletions api/agents/relevancy_agent.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Relevancy agent for determining relevancy of queries to database schema."""

import asyncio
import json
from .utils import BaseAgent, parse_response, run_completion

Expand Down Expand Up @@ -82,8 +83,14 @@ async def get_answer(self, user_question: str, database_desc: dict) -> dict:
}
)

answer = run_completion(
self.messages, self.custom_model, self.custom_api_key, temperature=0
# ``run_completion`` is synchronous. Awaiting it off-loop matters even
# though this method is already ``async``: the caller runs it as a task
# alongside table-finding, and a blocking call here would stall that
# task — and every other request — rather than overlap with it.
answer = await asyncio.to_thread(
run_completion,
self.messages, self.custom_model, self.custom_api_key,
label="relevancy", temperature=0,
)
self.messages.append({"role": "assistant", "content": answer})
return parse_response(answer)
1 change: 1 addition & 0 deletions api/agents/response_formatter_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def format_response(self, user_query: str, sql_query: str,

response = run_completion(
messages, self.custom_model, self.custom_api_key,
label="formatter",
temperature=0.3 # Slightly higher temperature for more natural responses
)
return response.strip()
Expand Down
Loading