refactor(sdk): make queryweaver_sdk pip-installable with shared core - #544
Conversation
- Add detailed assertions for query results (customer names, counts, etc.) - Add tests for filter queries, count aggregation, and joins - Validate SQL query structure and result data - Add session-scoped event loop to fix pytest-asyncio issues - Handle async event loop cleanup errors gracefully with skip - Expand model serialization tests
Disable warnings that are intentional architectural choices: - C0415: import-outside-toplevel (lazy imports for SDK) - W0718: broad-exception-caught (error handling) - R0902: too-many-instance-attributes (dataclasses) - R0903: too-few-public-methods - R0911: too-many-return-statements - R0913/R0917: too-many-arguments (SDK API design) - C0302: too-many-lines
- Extract SDK sync functions to new api/core/text2sql_sync.py module - Split QueryResult into composition: QueryResult + QueryMetadata + QueryAnalysis - Reduce local variables in query_database_sync with helper functions - Fix broad exception handling - use specific Redis/Connection/OS errors - Refactor query method to accept Union[str, QueryRequest] - Add compatibility properties to QueryResult for backwards compatibility - Document lazy imports in client.py module docstring Pylint score improved from 9.81/10 to 9.91/10 Remaining E0401 errors are missing dependencies in venv, not code issues
Resolve merge conflicts in .github/workflows/tests.yml, Makefile, and pyproject.toml. Standardize on uv (from staging migration) while preserving SDK additions from the PR branch: - tests.yml: Use uv instead of pipenv in sdk-tests job, keep SDK test exclusion in unit-tests, update action versions to v6 - Makefile: Use 'uv run' directly instead of pipenv/uv fallback, keep SDK targets (test-sdk, build-package, docker-test-services) - pyproject.toml: Keep SDK optional-dependencies structure (server/dev/all), use staging's dependency versions (~= notation), merge pytest/pylint config from both sides, keep [tool.uv] and [dependency-groups] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Use 'uv sync --all-extras' in tests, playwright, and pylint workflows to install server/dev optional dependencies (fastapi, uvicorn, etc.) - Fix imports in api/routes/graphs.py: import GENERAL_PREFIX and graph_name from text2sql_common, errors from api.core.errors - Remove unused variable 'last_message' in api/core/schema_loader.py - Add pylint disable comments for too-many-arguments/locals in api/core/text2sql_sync.py Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Use uv sync --all-extras in CI (needed for SDK optional deps) - Update dependency versions to staging's latest (litellm 1.82, falkordb 1.6, etc.) - Keep SDK optional dependency structure (core vs server vs dev) - Align playwright version across dependency groups (~=1.58.0) - Merge pylint config (max-line-length, ignore-patterns, messages_control) - Regenerate uv.lock Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bumps SDK to 0.2.0. Removes the api.extensions.db global mutation that
made two QueryWeaver instances collide, fixes the un-prefixed
database_id contract (breaking), and deduplicates orchestration
between the streaming and sync text2sql paths via shared helpers in
api.core.text2sql_common.
Key changes:
- api/core/db_resolver.py (new): lazy server-default fallback for db
- api/core/text2sql_common.py: 6 new shared helpers (execute_with_healing,
refresh_schema_if_modified, quote_identifiers_from_graph,
save_memory_background, format_ai_response,
build_destructive_confirmation_message); MESSAGE_DELIMITER moved here
- api/core/{result,request}_models.py (new): dataclasses extracted so
api.core no longer depends on queryweaver_sdk
- api/graph.py, loaders/*, memory/graphiti_tool.py: accept optional
db param; routes unchanged (use resolver fallback)
- queryweaver_sdk/client.py: no more api.extensions.db mutation;
close() awaits in-flight memory writes via contextvar-scoped sink
- queryweaver_sdk/models.py: re-export shim over api.core
- tests/test_sdk/: async-with fixtures, test_two_instances_isolated,
no more skip-on-RuntimeError wrappers
- CI: restored --locked --all-extras in all three workflows
Breaking (SDK 0.1.0 -> 0.2.0):
- DatabaseConnection.database_id is now un-prefixed (fixes double-
namespacing when passed back into query/delete/get_schema)
Fixes verified end-to-end: NL -> SQL -> results against live
PostgreSQL via SDK; FastAPI server boots cleanly with 21 routes and
HSTS middleware; two QueryWeaver instances coexist with independent
FalkorDB handles; uv build produces a working wheel; clean-venv
install succeeds.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completed Working on "Code Review"✅ Review publishing completed successfully. Posted comments from all chunks and submitted final review: COMMENT. Total comments: 8 across 8 files. ✅ Workflow completed successfully. |
|
🚅 Deployed to the QueryWeaver-pr-544 environment in queryweaver
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a Python SDK and package metadata, centralizes text-to-SQL utilities into a shared module, introduces a DB resolver for injectable FalkorDB handles, implements non-streaming SDK orchestration and sync APIs, threads optional db handles through loaders/memory tools, and adds CI, Docker test services, Makefile targets, examples, and integration tests. ChangesSDK Package Core
API Core Refactoring
Core Text2SQL Logic
Data Access Layer
API Routes
Build & Packaging
CI/CD Workflows
SDK Integration Tests
Documentation & Examples
Sequence DiagramsequenceDiagram
participant SDK as QueryWeaver SDK
participant Core as text2sql_sync
participant Agent as Analysis/FollowUp Agents
participant Exec as execute_with_healing
participant Loader as BaseLoader
participant DB as SQL DB / FalkorDB
participant Memory as MemoryTool
SDK->>Core: query_database_sync(user_id, graph_id, chat_data, db)
Core->>Agent: relevancy check + table discovery
Agent-->>Core: relevance & tables
alt Off-topic
Core-->>SDK: QueryResult(missing_information=...)
else Needs confirmation
Core-->>SDK: QueryResult(requires_confirmation=true)
else Proceed
Core->>Agent: analyze -> generate SQL
Agent-->>Core: sql_query + analysis
Core->>Exec: execute_with_healing(sql_query, loader_class, db_url, ...)
Exec->>Loader: run SQL
Loader->>DB: execute query
DB-->>Loader: rows/results
Exec-->>Core: results + execution metadata
Core->>Memory: save_memory_background(...)
Core-->>SDK: QueryResult(sql_query, results, metadata, analysis)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Thanks for the substantial refactor and test expansion. Consolidating the posted findings:
- 8 MAJOR comments (no BLOCKER/CRITICAL/MINOR/SUGGESTION/PRAISE in this review set)
- 8 files affected
Key themes
- SDK packaging/runtime boundary regressions: several findings indicate minimal
pip install queryweavermay still fail due to transitive server-only imports and dependency coupling. - Behavioral parity/correctness gaps: differences between SDK and streaming paths (memory persistence, error wrapping) can produce inconsistent outcomes and contracts.
- Test reliability/coverage risks: current fixture scoping and API-key gating reduce deterministic coverage for non-LLM SDK behaviors.
Recommended next steps
- Harden package boundaries so SDK imports and core SDK operations run on minimal install; add CI coverage for minimal-install smoke tests.
- Restore contract consistency by aligning error handling and memory-write behavior between SDK and streaming paths.
- Improve test determinism by removing unnecessary LLM-key gating for non-LLM tests and isolating mutable DB state per test.
Addressing these should materially reduce runtime surprises and improve confidence in the 0.2.0 SDK surface.
There was a problem hiding this comment.
Actionable comments posted: 20
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
api/loaders/graph_loader.py (1)
19-32:⚠️ Potential issue | 🟡 MinorAnnotate the new optional graph DB handle.
load_to_graphnow exposes a dependency-injection parameter; keep the loader API typed. As per coding guidelines,api/**/*.py: Include type hints throughout Python code.Proposed fix
import json +from typing import Any import tqdm @@ - db=None, + db: Any | None = None, ) -> None:🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@api/loaders/graph_loader.py` around lines 19 - 32, The new optional DB handle parameter on load_to_graph needs a type annotation; update the function signature for load_to_graph to type the db parameter as Optional[...] (e.g., Optional[FalkorDB] or the concrete DB interface used by resolve_db) and import Optional from typing (or use typing.Optional), keeping the existing -> None return type; ensure any references to resolve_db(db) still type-check with the chosen DB type and update any stub/type imports (FalkorDB or GraphDB interface) so the annotation is valid.api/graph.py (1)
337-350:⚠️ Potential issue | 🟠 MajorPreserve column-only search results.
When the LLM returns only column descriptions,
main_taskscontains one task, buttables_by_columns_desbecomes[]; those matches are silently discarded.Proposed fix
# Execute the main embedding-based searches in parallel results = await asyncio.gather(*main_tasks) # Unpack results based on what tasks we ran - tables_des = results[0] if table_embeddings else [] - tables_by_columns_des = results[1] if (table_embeddings and column_embeddings) else [] + result_index = 0 + tables_des = [] + tables_by_columns_des = [] + if table_embeddings: + tables_des = results[result_index] + result_index += 1 + if column_embeddings: + tables_by_columns_des = results[result_index]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@api/graph.py` around lines 337 - 350, The current unpacking logic assumes fixed positions in results and drops column-only searches; instead iterate results in insertion order and assign them based on which tasks were appended to main_tasks: after building main_tasks with _find_tables and/or _find_tables_by_columns, consume results sequentially (use an index or pop from a list) and set tables_des when table_embeddings was requested and tables_by_columns_des when column_embeddings was requested so that a single-task run (column-only or table-only) maps correctly to the right variable (refer to main_tasks, _find_tables, _find_tables_by_columns, results, tables_des, tables_by_columns_des).api/loaders/mysql_loader.py (1)
469-482:⚠️ Potential issue | 🔴 CriticalFix the async generator consumption in
refresh_graph_schema.
MySQLLoader.load()is an async generator (containsyieldstatements), soawait MySQLLoader.load(...)at line 482 raisesTypeError. Additionally, deleting the graph at line 470 before reloading creates a risky window where the schema could be lost if reload fails. Useasync forto properly consume the async generator:Proposed fix
- success, message = await MySQLLoader.load(prefix, db_url, db=db) + success = False + message = "Failed to reload schema" + async for success, message in MySQLLoader.load(prefix, db_url, db=db): + pass🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@api/loaders/mysql_loader.py` around lines 469 - 482, The code calls await MySQLLoader.load(...) but MySQLLoader.load is an async generator, so replace the await with an async for loop to consume yielded items (e.g., "async for result in MySQLLoader.load(prefix, db_url, db=db):" and capture success/message from the yielded values); also avoid deleting the existing graph before a successful reload—first run the reload into a temporary/staging graph or buffer and only call graph.delete() and replace the old graph after the async for confirms success (adjust the logic in refresh_graph_schema to use the async-for consumption of MySQLLoader.load and perform the delete/replace only after a successful load).api/loaders/postgres_loader.py (2)
510-523:⚠️ Potential issue | 🔴 CriticalConsume the async generator instead of awaiting it.
PostgresLoader.load()yields progress tuples, soawait PostgresLoader.load(...)raisesTypeError. Since line 511 deletes the graph first, schema refresh can drop the graph and then fail to reload it.Proposed fix
- success, message = await PostgresLoader.load(prefix, db_url, db=db) + success = False + message = "Failed to reload schema" + async for success, message in PostgresLoader.load(prefix, db_url, db=db): + pass🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@api/loaders/postgres_loader.py` around lines 510 - 523, The code awaits PostgresLoader.load(...) even though load is an async generator and also deletes the graph before reloading which can cause reload failures; fix by first running the reload by iterating the async generator (use "async for progress in PostgresLoader.load(prefix, db_url, db=db)" to consume progress tuples and capture the final (success, message)), then only call await graph.delete() or delete the old graph after confirming success; reference resolve_db.select_graph(graph_id), graph.delete(), graph_id, prefix, and PostgresLoader.load to locate the code to change.
177-180:⚠️ Potential issue | 🟠 MajorUse
urlparsefor PostgreSQL database-name extraction.The current split-based extraction fails for valid PostgreSQL URLs with Unix-socket parameters, e.g.,
postgresql:///mydb?host=/var/run/postgresql, producing the scheme name instead of the database name. This causes incorrect graph name generation.Proposed fix
- # Extract database name from connection URL - db_name = connection_url.split('/')[-1] - if '?' in db_name: - db_name = db_name.split('?')[0] + # Extract database name from connection URL + parsed_url = urlparse(connection_url) + db_name = unquote(parsed_url.path.lstrip("/")) + if not db_name: + raise ValueError("PostgreSQL connection URL must include a database name")Note:
urlparseandunquoteare already imported.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@api/loaders/postgres_loader.py` around lines 177 - 180, Replace the brittle split-based extraction of db_name with urlparse/unquote: parse connection_url via urlparse(connection_url), take parsed.path, strip any leading '/' (parsed.path.lstrip('/')), unquote the result to handle percent-encoding, and assign that to db_name; keep a fallback (e.g., original split logic) only if the parsed path is empty. Update the code that sets db_name (the variable assigned from connection_url) to use this parsing logic so Unix-socket and query-parameter URLs yield the correct database name.
🧹 Nitpick comments (7)
.github/workflows/tests.yml (1)
121-124: Pinuvto a consistent version across jobs.Other steps in this workflow (and the wider repo) pin
version: "0.7.12"(see line 42 in this file, and.github/workflows/playwright.yml). This newsdk-testsjob uses"latest", which can drift and make CI non-reproducible.🔧 Suggested fix
- name: Install uv uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: - version: "latest" + version: "0.7.12"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/tests.yml around lines 121 - 124, The "Install uv" step uses astral-sh/setup-uv with version: "latest" which can cause CI drift; change the with.version value to the pinned release used elsewhere (e.g., "0.7.12") so the sdk-tests job matches other workflows; locate the step that references astral-sh/setup-uv (the "Install uv" step) and replace version: "latest" with version: "0.7.12".Makefile (2)
83-86: Prefer--waitover a fixed sleep.
docker-compose.test.ymlalready declares healthchecks for falkordb/postgres/mysql, sodocker compose up -d --waitwill block until each service is healthy. The currentsleep 10is flaky on slow runners and unnecessarily slow on fast ones.🔧 Suggested fix
docker-test-services: ## Start all test services (FalkorDB + PostgreSQL + MySQL) - docker compose -f docker-compose.test.yml up -d - `@echo` "Waiting for services to be ready..." - `@sleep` 10 + docker compose -f docker-compose.test.yml up -d --wait --wait-timeout 120🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Makefile` around lines 83 - 86, The docker-test-services Makefile target uses a fixed sleep which is flaky; update the docker compose invocation in the docker-test-services recipe (the `docker compose -f docker-compose.test.yml up -d` line) to use `--wait` (i.e., `docker compose -f docker-compose.test.yml up -d --wait`) so it blocks until the healthchecks in docker-compose.test.yml pass, and remove the subsequent `sleep 10` (and optional "Waiting for services to be ready..." echo) to rely on the built-in wait behavior.
1-1: Add new targets to.PHONY.
test-sdk-quick(line 94) andtest-all(line 97) are new phony targets not listed in.PHONY, somakewill treat them as file targets if a file of the same name appears.🔧 Suggested fix
-.PHONY: help install test test-unit test-e2e test-e2e-headed lint format clean setup-dev build lint-frontend test-sdk docker-test-services docker-test-stop build-package +.PHONY: help install test test-unit test-e2e test-e2e-headed lint format clean setup-dev build lint-frontend test-sdk test-sdk-quick test-all docker-test-services docker-test-stop build-package🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Makefile` at line 1, The Makefile's .PHONY declaration is missing the newly added phony targets test-sdk-quick and test-all; update the .PHONY line (the .PHONY entry at the top of the Makefile) to include test-sdk-quick and test-all so make treats them as phony targets rather than files, ensuring the targets defined later in the file are always executed.queryweaver_sdk/client.py (1)
207-215: Redundant per-lineimport-outside-topleveldisables.The module-level
# pylint: disable=import-outside-toplevelat line 24 already covers the whole file, so the inline disables on lines 213 and 214 are noise. Drop them for consistency with the other lazy imports in this file (e.g. lines 91, 120, 156‑157, 200, 232, 251, 273‑274 which rely on the module-level directive).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@queryweaver_sdk/client.py` around lines 207 - 215, Remove the redundant inline pylint disables on the lazy imports inside the async method list_databases: delete the trailing "# pylint: disable=import-outside-toplevel" comments from the two import statements that import list_databases from api.core.schema_loader and GENERAL_PREFIX from api.core.text2sql_common so the method uses the module-level disable already present; ensure the imports remain inside list_databases (preserving lazy import behavior) and run linters to confirm no new warnings.tests/test_sdk/conftest.py (1)
147-154:queryweaverfixture swallows close errors silently.If
await qw.close()raises (e.g. Redis pool already disconnected by a test), the exception becomes a teardown error that masks the actual test failure. Consider wrapping teardown in try/except-and-log, or useasync with QueryWeaver(...)inside the fixture for symmetric lifecycle handling:-@pytest.fixture -async def queryweaver(falkordb_url): - """Provide initialized QueryWeaver instance with proper teardown.""" - from queryweaver_sdk import QueryWeaver - - qw = QueryWeaver(falkordb_url=falkordb_url, user_id="test_user") - yield qw - await qw.close() +@pytest.fixture +async def queryweaver(falkordb_url): + """Provide initialized QueryWeaver instance with proper teardown.""" + from queryweaver_sdk import QueryWeaver + + async with QueryWeaver(falkordb_url=falkordb_url, user_id="test_user") as qw: + yield qw🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/test_sdk/conftest.py` around lines 147 - 154, The queryweaver fixture currently yields a QueryWeaver instance and calls await qw.close() in teardown which can raise and mask test failures; update the fixture to either use symmetric lifecycle by creating the instance inside an async context (async with QueryWeaver(falkordb_url=..., user_id="test_user") as qw: yield qw) or wrap the teardown call in a try/except that catches exceptions from qw.close() and logs them (referencing the fixture name queryweaver, the QueryWeaver constructor, and the qw.close() method) so close errors don't obscure test failures.tests/test_sdk/test_queryweaver.py (1)
24-30: Sync init tests leak aFalkorDBConnectionobject.
test_init_defaultsandtest_init_with_custom_user_idconstructQueryWeaverbut neverclose()it. Because_dbis lazy this won't open a real Redis pool today, but the pattern is fragile — if__init__ever starts eagerly probing the connection (e.g. for a version check), these tests will start leaking live connections. Useasync with(with@pytest.mark.asyncio) or explicitlyawait qw.close()to be future‑proof.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/test_sdk/test_queryweaver.py` around lines 24 - 30, The tests test_init_defaults and test_init_with_custom_user_id create QueryWeaver instances that may allocate a FalkorDBConnection; update each test to ensure the connection is closed by either turning the test into an async test (add `@pytest.mark.asyncio`) and using "async with QueryWeaver(...)" or by explicitly awaiting qw.close() at the end of the test; reference QueryWeaver and its close() method (or the context-manager protocol for QueryWeaver) so the FalkorDBConnection held in _db is properly cleaned up to prevent leaking live connections.queryweaver_sdk/connection.py (1)
83-86: Avoid direct access to FalkorDB client's internal connection attribute — use the publicaclose()API instead.When constructed via
FalkorDB(host=host, port=port)(lines 84),self._poolremainsNoneandclose()falls into theelifbranch that callsself._db.connection.aclose()(line 118). This directly accesses an undocumented internal attribute of the FalkorDB client. The public API providesaclose()method on the FalkorDB client itself, which properly handles the underlying connection cleanup.Consider either:
- Simpler fix: Replace
await self._db.connection.aclose()withawait self._db.aclose()(the public API), or- Unified approach: Build a connection URL from host/port and use
BlockingConnectionPool.from_url()for both code paths, ensuring consistent teardown viapool.disconnect().🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@queryweaver_sdk/connection.py` around lines 83 - 86, The close path currently calls the FalkorDB client's undocumented internal attribute (self._db.connection.aclose()); update the teardown to use the public API by replacing that call with await self._db.aclose() in the close() method of the connection class (or alternatively refactor construction to use BlockingConnectionPool.from_url() for both creation branches and call pool.disconnect() for a unified cleanup); locate references to FalkorDB, close(), self._db.connection.aclose(), and adjust them to use the public aclose() or the BlockingConnectionPool.from_url()/pool.disconnect() approach so the client is torn down via its supported API.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/tests.yml:
- Around line 136-143: The "Run SDK tests" job will run without OPENAI_API_KEY
for forked PRs causing LLM tests to fail; update the workflow so LLM-dependent
tests only run when the secret is present by making one of these changes: add a
job-level condition like if: ${{ secrets.OPENAI_API_KEY != '' }} to the "Run SDK
tests" job (or split the job into a non-LLM "test-sdk-quick" job and a separate
"Run SDK LLM tests" job gated by the secret or by a label/pull_request_target
with safeguards), or add an early pre-flight step in "Run SDK tests" that checks
OPENAI_API_KEY and fails with a clear message if empty; ensure the change
references the OPENAI_API_KEY env var used in the job so LLM tests are skipped
when the secret is unavailable.
In `@api/core/db_resolver.py`:
- Around line 12-18: Add explicit type hints to the resolve_db boundary: import
typing (e.g., "from typing import Optional, Any") and change the signature of
resolve_db to accept db: Optional[Any] = None and return Any, and annotate the
lazily imported api.extensions.db (imported as _default_db) as the same type so
the function returns a typed handle; update any local comments or # type: ignore
as needed when importing _default_db to satisfy the type checker.
In `@api/core/text2sql_common.py`:
- Around line 140-143: The function detect_destructive_operation currently uses
sql_query.strip().split()[0] which misclassifies queries with leading comments
(e.g., "-- comment\nDROP TABLE ..."); update detect_destructive_operation to
first remove leading SQL comments and whitespace before extracting the first
token: skip or strip initial single-line comments beginning with "--" and
initial block comments "/* ... */" (repeated until the first non-comment token),
then extract the verb, uppercase it, and check against DESTRUCTIVE_OPS while
safely handling empty/whitespace-only input.
- Around line 67-71: The code currently raises GraphNotFoundError for an
invalid/blank graph_id; change this to raise InvalidArgumentError instead so
callers treat it as a 400 Bad Request. Locate the block where graph_id is
normalized (graph_id = graph_id.strip()[:200]) and replace the
GraphNotFoundError instantiation with an InvalidArgumentError containing the
same or similar message ("Invalid graph_id, must be less than 200 characters.").
Ensure the exception import or reference for InvalidArgumentError is
present/used in text2sql_common.py so the new exception type is available.
In `@api/core/text2sql_sync.py`:
- Around line 412-416: The SDK sync path currently allows destructive queries
against general/demo graphs; update the destructive-op guard in the sync
handlers so destructive operations on general graphs are intercepted: in
query_database_sync() (around the block at 412-416) and in
execute_destructive_operation_sync() (the block around 452-480) change/add the
check to return _confirmation_required_result(ctx, analysis) when
analysis.is_destructive and is_general_graph(ctx.db.graph_id) so that
destructive operations against demo/general graphs require confirmation instead
of being executed.
- Around line 152-158: Add the same per-request override validation used in the
streaming path before constructing _ChatContext: validate getattr(chat_data,
'custom_model', None) follows the "vendor/model" pattern with vendor one of
(openai, anthropic, gemini, azure, ollama, cohere) and validate
getattr(chat_data, 'custom_api_key', None) is either None or has length >= 10;
if either check fails, raise a clear ValueError (or similar) so invalid
overrides are rejected before creating chat_ctx in text2sql_sync.py where
_ChatContext is instantiated.
In `@api/graph.py`:
- Around line 39-41: Annotate all parameters that default to None with explicit
Optional types and add the required typing imports; e.g., update
get_db_description(graph_id: str, db=None) -> tuple[str, str] to something like
get_db_description(graph_id: str, db: Optional[Any] = None) -> tuple[str, str]
(import Optional, Any from typing) and similarly change any parameter like
db_description: str = None to db_description: Optional[str] = None; apply the
same pattern to the other affected functions (the ones around lines 57-59,
73-76, 87, and 279-283) and ensure typing imports are added at the top of the
module.
In `@api/memory/graphiti_tool.py`:
- Around line 60-64: The constructor currently accepts an untyped db injection;
add explicit type hints to the public API by typing the db parameter and the
instance attribute: annotate GraphitiTool.__init__(self, user_id: str, graph_id:
str, db: Optional[DatabaseType] = None) (choose the concrete alias used in the
project, e.g., Optional[Mapping[str, Any]] or the project's DB protocol), and
annotate self._db and self.memory_db_name where appropriate; update resolve_db
return type or cast to the chosen DatabaseType and also apply the same typing
fix to the other constructor/initializer block referenced around lines 82-90 so
all injected db parameters and stored attributes are consistently typed
(reference symbols: __init__, self._db, self.memory_db_name, resolve_db,
FalkorDriver).
In `@docker-compose.test.yml`:
- Around line 5-13: The docker-compose service "falkordb" currently uses an
unpinned image ("falkordb/falkordb:latest"), which breaks test reproducibility;
change the "image" field in docker-compose.test.yml for the falkordb service to
a specific, tested tag (e.g., "falkordb/falkordb:<version>") instead of :latest,
and ensure that this pinned version matches the one used in CI or test docs
(update CI/test config if necessary).
In `@pyproject.toml`:
- Around line 120-123: The pyproject.toml pylint section ([tool.pylint.main])
currently sets ignore-patterns to ["test_.*\\.py", "conftest\\.py"], which
excludes test files from linting; remove or clear the ignore-patterns setting so
pylint runs over all Python files (update or delete the ignore-patterns key
under [tool.pylint.main], leaving max-line-length = 120 intact) to ensure tests
are included in the project-wide lint job.
- Around line 67-68: The console-script entry "queryweaver = \"api.index:main\""
points to a non-existent function; either add a main() function in api/index.py
that initializes and runs the CLI entrypoint, or change the entry to
"api.index:app" if the intended symbol is the existing ASGI/FastAPI app; update
the pyproject.toml entry accordingly and ensure the referenced symbol (main or
app) is exported at module level in api.index.
In `@queryweaver_sdk/client.py`:
- Around line 287-295: The current close() snapshots _pending_tasks once and
awaits them, missing any tasks scheduled during that await; change close() to
repeatedly drain until no tasks remain by looping: while self._pending_tasks:
take a snapshot (e.g., tasks = list(self._pending_tasks)), await
asyncio.gather(*tasks, return_exceptions=True) (optionally await
asyncio.sleep(0) to allow scheduling), and repeat until the set is empty, then
call await self._connection.close(); ensure you reference the close method, the
_pending_tasks collection, and _connection when making this change.
- Around line 254-285: The execute_confirmed method currently ignores
per-request LLM overrides and must accept and forward them: update
execute_confirmed's signature to accept custom_api_key and custom_model (or a
QueryRequest-style wrapper) and populate the ConfirmRequest with those fields
when constructing confirm_data; then pass the updated confirm_data through to
execute_destructive_operation_sync (and/or pass through any wrapper) so the same
per-request LLM credentials used by query()/ChatRequest are honored during
confirmation. Ensure to reference execute_confirmed, ConfirmRequest, and
execute_destructive_operation_sync when making the changes.
- Around line 103-121: connect_database currently calls load_database_sync
directly and therefore does not register any background work with the client's
task sink; to make behavior consistent with query() and execute_confirmed(),
wrap the load_database_sync call with the client's _bind_task_sink so any
background tasks get added to _pending_tasks and will be awaited in close();
locate the connect_database method and replace the direct await
load_database_sync(...) call with an awaited call into _bind_task_sink(...)
passing the same coroutine (referencing connect_database, _bind_task_sink,
_pending_tasks, close, and load_database_sync), or if the omission is
intentional add a clear inline comment in connect_database explaining why schema
loading should not be tracked.
In `@queryweaver_sdk/connection.py`:
- Around line 111-119: The close() method currently clears _db/_pool but allows
the db property to lazily recreate connections; add a boolean attribute _closed
on the Connection class, set self._closed = True at the end of close() (and
ensure close() is idempotent), and update the db property getter to check
self._closed and raise RuntimeError("Connection is closed") if true so
post-__aexit__ calls cannot recreate resources; reference the existing close()
method and the db property in your changes.
In `@README.md`:
- Around line 299-304: The README example shows using QueryWeaver instances
(QueryWeaver(...)) and then calling a.query(...) / b.query(...) without
connecting databases first; update the snippet to call the instance method
connect_database (e.g., await a.connect_database("sales") and await
b.connect_database("ops")) before invoking query, or alternatively add a clear
"illustrative only" note above the block; reference QueryWeaver,
connect_database, and query so the change is applied to the correct example.
In `@tests/test_sdk/conftest.py`:
- Around line 8-18: The pytest test markers registered in pytest_configure
currently add only dependency gates (requires_llm, requires_postgres,
requires_mysql); add guideline-required markers by registering "integration" and
"slow" (and optionally "e2e", "auth", "unit") via additional
config.addinivalue_line calls inside pytest_configure, then annotate the real
integration tests (e.g., module tests/test_sdk/test_queryweaver.py) with
module-level pytest marks such as pytestmark = [pytest.mark.integration,
pytest.mark.slow] so the suite is correctly categorized; update the
pytest_configure function and add the pytestmark declaration in
test_queryweaver.py.
- Around line 49-91: The try block creating and using psycopg2's conn and cursor
can leak resources if an exception is raised; update the block that opens conn
and cursor to use context managers (with conn, with conn.cursor()) or ensure
conn.close() and cursor.close() are called in a finally block so the DB
connection/session is always closed before calling pytest.skip; apply the same
fix pattern to the MySQL fixture that uses conn and cursor to prevent leaking
connections on exceptions.
In `@tests/test_sdk/test_queryweaver.py`:
- Around line 269-357: The TestModels class in
tests/test_sdk/test_queryweaver.py lacks a pytest marker; add a unit marker by
importing pytest (if not present) and either decorate the TestModels class with
`@pytest.mark.unit` or add pytestmark = [pytest.mark.unit] at the top of that
class to mark these pure dataclass tests as unit tests (refer to the TestModels
class name to locate the change).
- Around line 87-90: The test test_connect_invalid_url currently uses a broad
pytest.raises(Exception); update it to assert the documented exception by using
pytest.raises(ValueError) when calling
QueryWeaver.connect_database("invalid://url") so the test specifically expects
ValueError (the exception noted in QueryWeaver.connect_database docstring) and
will fail if a different exception type is raised.
---
Outside diff comments:
In `@api/graph.py`:
- Around line 337-350: The current unpacking logic assumes fixed positions in
results and drops column-only searches; instead iterate results in insertion
order and assign them based on which tasks were appended to main_tasks: after
building main_tasks with _find_tables and/or _find_tables_by_columns, consume
results sequentially (use an index or pop from a list) and set tables_des when
table_embeddings was requested and tables_by_columns_des when column_embeddings
was requested so that a single-task run (column-only or table-only) maps
correctly to the right variable (refer to main_tasks, _find_tables,
_find_tables_by_columns, results, tables_des, tables_by_columns_des).
In `@api/loaders/graph_loader.py`:
- Around line 19-32: The new optional DB handle parameter on load_to_graph needs
a type annotation; update the function signature for load_to_graph to type the
db parameter as Optional[...] (e.g., Optional[FalkorDB] or the concrete DB
interface used by resolve_db) and import Optional from typing (or use
typing.Optional), keeping the existing -> None return type; ensure any
references to resolve_db(db) still type-check with the chosen DB type and update
any stub/type imports (FalkorDB or GraphDB interface) so the annotation is
valid.
In `@api/loaders/mysql_loader.py`:
- Around line 469-482: The code calls await MySQLLoader.load(...) but
MySQLLoader.load is an async generator, so replace the await with an async for
loop to consume yielded items (e.g., "async for result in
MySQLLoader.load(prefix, db_url, db=db):" and capture success/message from the
yielded values); also avoid deleting the existing graph before a successful
reload—first run the reload into a temporary/staging graph or buffer and only
call graph.delete() and replace the old graph after the async for confirms
success (adjust the logic in refresh_graph_schema to use the async-for
consumption of MySQLLoader.load and perform the delete/replace only after a
successful load).
In `@api/loaders/postgres_loader.py`:
- Around line 510-523: The code awaits PostgresLoader.load(...) even though load
is an async generator and also deletes the graph before reloading which can
cause reload failures; fix by first running the reload by iterating the async
generator (use "async for progress in PostgresLoader.load(prefix, db_url,
db=db)" to consume progress tuples and capture the final (success, message)),
then only call await graph.delete() or delete the old graph after confirming
success; reference resolve_db.select_graph(graph_id), graph.delete(), graph_id,
prefix, and PostgresLoader.load to locate the code to change.
- Around line 177-180: Replace the brittle split-based extraction of db_name
with urlparse/unquote: parse connection_url via urlparse(connection_url), take
parsed.path, strip any leading '/' (parsed.path.lstrip('/')), unquote the result
to handle percent-encoding, and assign that to db_name; keep a fallback (e.g.,
original split logic) only if the parsed path is empty. Update the code that
sets db_name (the variable assigned from connection_url) to use this parsing
logic so Unix-socket and query-parameter URLs yield the correct database name.
---
Nitpick comments:
In @.github/workflows/tests.yml:
- Around line 121-124: The "Install uv" step uses astral-sh/setup-uv with
version: "latest" which can cause CI drift; change the with.version value to the
pinned release used elsewhere (e.g., "0.7.12") so the sdk-tests job matches
other workflows; locate the step that references astral-sh/setup-uv (the
"Install uv" step) and replace version: "latest" with version: "0.7.12".
In `@Makefile`:
- Around line 83-86: The docker-test-services Makefile target uses a fixed sleep
which is flaky; update the docker compose invocation in the docker-test-services
recipe (the `docker compose -f docker-compose.test.yml up -d` line) to use
`--wait` (i.e., `docker compose -f docker-compose.test.yml up -d --wait`) so it
blocks until the healthchecks in docker-compose.test.yml pass, and remove the
subsequent `sleep 10` (and optional "Waiting for services to be ready..." echo)
to rely on the built-in wait behavior.
- Line 1: The Makefile's .PHONY declaration is missing the newly added phony
targets test-sdk-quick and test-all; update the .PHONY line (the .PHONY entry at
the top of the Makefile) to include test-sdk-quick and test-all so make treats
them as phony targets rather than files, ensuring the targets defined later in
the file are always executed.
In `@queryweaver_sdk/client.py`:
- Around line 207-215: Remove the redundant inline pylint disables on the lazy
imports inside the async method list_databases: delete the trailing "# pylint:
disable=import-outside-toplevel" comments from the two import statements that
import list_databases from api.core.schema_loader and GENERAL_PREFIX from
api.core.text2sql_common so the method uses the module-level disable already
present; ensure the imports remain inside list_databases (preserving lazy import
behavior) and run linters to confirm no new warnings.
In `@queryweaver_sdk/connection.py`:
- Around line 83-86: The close path currently calls the FalkorDB client's
undocumented internal attribute (self._db.connection.aclose()); update the
teardown to use the public API by replacing that call with await
self._db.aclose() in the close() method of the connection class (or
alternatively refactor construction to use BlockingConnectionPool.from_url() for
both creation branches and call pool.disconnect() for a unified cleanup); locate
references to FalkorDB, close(), self._db.connection.aclose(), and adjust them
to use the public aclose() or the
BlockingConnectionPool.from_url()/pool.disconnect() approach so the client is
torn down via its supported API.
In `@tests/test_sdk/conftest.py`:
- Around line 147-154: The queryweaver fixture currently yields a QueryWeaver
instance and calls await qw.close() in teardown which can raise and mask test
failures; update the fixture to either use symmetric lifecycle by creating the
instance inside an async context (async with QueryWeaver(falkordb_url=...,
user_id="test_user") as qw: yield qw) or wrap the teardown call in a try/except
that catches exceptions from qw.close() and logs them (referencing the fixture
name queryweaver, the QueryWeaver constructor, and the qw.close() method) so
close errors don't obscure test failures.
In `@tests/test_sdk/test_queryweaver.py`:
- Around line 24-30: The tests test_init_defaults and
test_init_with_custom_user_id create QueryWeaver instances that may allocate a
FalkorDBConnection; update each test to ensure the connection is closed by
either turning the test into an async test (add `@pytest.mark.asyncio`) and using
"async with QueryWeaver(...)" or by explicitly awaiting qw.close() at the end of
the test; reference QueryWeaver and its close() method (or the context-manager
protocol for QueryWeaver) so the FalkorDBConnection held in _db is properly
cleaned up to prevent leaking live connections.
🪄 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: e4ea8b59-e4f1-420a-b1e0-1cab480fd0e5
⛔ Files ignored due to path filters (2)
app/package-lock.jsonis excluded by!**/package-lock.jsonuv.lockis excluded by!**/*.lock
📒 Files selected for processing (30)
.github/wordlist.txt.github/workflows/playwright.yml.github/workflows/pylint.yml.github/workflows/tests.ymlMakefileREADME.mdapi/core/__init__.pyapi/core/db_resolver.pyapi/core/request_models.pyapi/core/result_models.pyapi/core/schema_loader.pyapi/core/text2sql.pyapi/core/text2sql_common.pyapi/core/text2sql_sync.pyapi/graph.pyapi/loaders/graph_loader.pyapi/loaders/mysql_loader.pyapi/loaders/postgres_loader.pyapi/memory/graphiti_tool.pyapi/routes/database.pyapi/routes/graphs.pydocker-compose.test.ymlpyproject.tomlqueryweaver_sdk/__init__.pyqueryweaver_sdk/client.pyqueryweaver_sdk/connection.pyqueryweaver_sdk/models.pytests/test_sdk/__init__.pytests/test_sdk/conftest.pytests/test_sdk/test_queryweaver.py
💤 Files with no reviewable changes (1)
- api/routes/database.py
There was a problem hiding this comment.
Pull request overview
This PR refactors QueryWeaver to make the Python SDK (queryweaver_sdk) cleanly pip-installable while sharing core Text2SQL logic with the server path, eliminating reliance on hidden global api.extensions.db mutation and deduplicating orchestration between streaming and non-streaming flows.
Changes:
- Introduces shared core helpers (
api/core/text2sql_common.py) and a non-streaming orchestrator (api/core/text2sql_sync.py) used by the SDK. - Adds lazy FalkorDB resolution via
resolve_db()and threads optionaldb=through core/loader/memory/graph utilities to avoid global mutation. - Adds/updates SDK packaging, models, docs, CI, and a new SDK integration test suite with docker-compose test services.
Reviewed changes
Copilot reviewed 30 out of 32 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| uv.lock | Bumps package to 0.2.0 and updates extras/deps resolution. |
| pyproject.toml | Defines SDK-minimal deps, optional extras, and wheel/sdist packaging for api/ + queryweaver_sdk/. |
| queryweaver_sdk/init.py | SDK public exports and version 0.2.0. |
| queryweaver_sdk/client.py | Main SDK client; lazy imports; passes explicit db=; awaits background memory tasks on close. |
| queryweaver_sdk/connection.py | Async FalkorDB connection wrapper for SDK lifecycle management. |
| queryweaver_sdk/models.py | Re-export shim to shared api.core.*_models. |
| api/core/db_resolver.py | Adds resolve_db() for lazy fallback to server singleton DB handle. |
| api/core/text2sql_common.py | New shared helpers/constants (healing, schema refresh, quoting, memory background tasks, graph naming). |
| api/core/text2sql_sync.py | New non-streaming Text2SQL orchestration for SDK, built on shared helpers. |
| api/core/text2sql.py | Refactors streaming path to reuse shared helpers and accept optional db=. |
| api/core/schema_loader.py | Threads optional db=; adds load_database_sync() returning structured SDK result; uses urlparse. |
| api/core/result_models.py | New shared result dataclasses for both SDK and server. |
| api/core/request_models.py | New shared request dataclass (QueryRequest). |
| api/core/init.py | Re-exports common helpers/constants from text2sql_common. |
| api/graph.py | Adds optional db= and uses resolve_db() for graph operations. |
| api/loaders/graph_loader.py | Accepts optional db= for graph writes; uses resolve_db(). |
| api/loaders/postgres_loader.py | Threads db= through loader and refresh flow. |
| api/loaders/mysql_loader.py | Threads db= through loader and refresh flow. |
| api/memory/graphiti_tool.py | Avoids importing global db; accepts optional db= and resolves lazily. |
| api/routes/graphs.py | Switches to graph_name() + centralized error imports. |
| api/routes/database.py | Removes duplicate MESSAGE_DELIMITER constant (now in common module). |
| tests/test_sdk/init.py | Marks SDK test module. |
| tests/test_sdk/conftest.py | Adds SDK integration fixtures and markers; sets up test schemas. |
| tests/test_sdk/test_queryweaver.py | Adds/reworks SDK integration tests, including multi-instance isolation regression test. |
| docker-compose.test.yml | Adds local test services for SDK integration testing. |
| README.md | Documents SDK install/usage, multi-instance behavior, and destructive-operation confirmation. |
| Makefile | Adds package build + SDK test targets; excludes SDK from unit-test target. |
| .github/workflows/tests.yml | Restores locked sync w/ extras; adds separate sdk-tests job with DB services. |
| .github/workflows/pylint.yml | Uses locked sync w/ extras for lint workflow. |
| .github/workflows/playwright.yml | Uses locked sync w/ extras for Playwright workflow. |
| .github/wordlist.txt | Adds “SDK” to spellcheck wordlist. |
| app/package-lock.json | Updates lockfile metadata (peer flags) as part of dependency state. |
Files not reviewed (1)
- app/package-lock.json: Language not supported
- RefreshResult.tables_updated was always 0 (load_database_sync never set tables_loaded); drop the field rather than misreport. - refresh_database_schema_sync now checks is_general_graph on the namespaced id, matching the streaming refresh path. - Reword stale MESSAGE_DELIMITER docstring reference in routes/database.py. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
api/routes/database.py (1)
7-7: 💤 Low valueConsider importing
load_databasefrom theapi.corepublic surface.The PR centralizes exports in
api/core/__init__.py(which includesload_databasein__all__), but this route still reaches into the internal submodule. Updating the import keeps the layering consistent with the PR's stated refactoring goal.♻️ Suggested change
-from api.core.schema_loader import load_database +from api.core import load_database🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@api/routes/database.py` at line 7, The import in this route reaches into the internal module api.core.schema_loader; change it to import the public symbol from the package export instead by replacing the direct submodule import with "from api.core import load_database" so the route uses the centralized public surface (symbol: load_database) rather than the internal module (schema_loader).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@api/core/text2sql_sync.py`:
- Line 480: Move the await MemoryTool.create(user_id, graph_id, db=db) call into
the existing try block so its errors are caught and transformed into the
function's structured QueryResult; specifically locate the MemoryTool.create
invocation in text2sql_sync.py and wrap or relocate it inside the same try that
covers other I/O calls (the try/except that returns a QueryResult on failure),
ensuring MemoryTool.create errors are handled by the existing except path rather
than propagating raw exceptions.
---
Nitpick comments:
In `@api/routes/database.py`:
- Line 7: The import in this route reaches into the internal module
api.core.schema_loader; change it to import the public symbol from the package
export instead by replacing the direct submodule import with "from api.core
import load_database" so the route uses the centralized public surface (symbol:
load_database) rather than the internal module (schema_loader).
🪄 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: 92b4f4e9-f2f3-414f-a91e-3bdb72b32813
📒 Files selected for processing (3)
api/core/result_models.pyapi/core/text2sql_sync.pyapi/routes/database.py
🚧 Files skipped from review as they are similar to previous changes (1)
- api/core/result_models.py
Four CI failures resolved. build / unit-tests (pylint duplicate-code R0801): api/core/text2sql.py:472–482 inlined the healing loop, duplicating api/core/pipeline.py:338–348 (execute_with_healing). With text2sql_sync.py deleted in dd26c74, pipeline.execute_with_healing has zero callers — same for refresh_schema_if_modified, _maybe_emit, EmitFn. Deleting them eliminates the duplication and ~120 lines of dead code. Restored the validate_and_truncate_chat return statement that an earlier sed accidentally trimmed. test (npm ci EUSAGE): Root package-lock.json was out of sync with the workspace's package.json after the staging merge brought in tailwind-merge 3.5.0 / vite 7.3.2. Regenerated via npm install. sdk-tests: test_connect_postgres / test_connect_mysql were failing on fork PRs because OPENAI_API_KEY is empty there → connect_database returns success=False, database_id="" → the finally cleanup hit the new InvalidArgumentError I added in graph_name. Two fixes: re-add the has_llm_key gate (these tests do need an LLM for embeddings) and guard the cleanup with `if result.success and result.database_id`. CodeQL "Information exposure through an exception" on api/routes/graphs.py:157 / :183: Wrapped the inner stream() generators in try/except so any exception raised mid-iteration logs internally and yields a generic error event, instead of letting the traceback surface to the client. Verified: pylint 10/10, full SDK test suite (14 pass, 9 skip — same as fork-PR CI), SDK end-to-end against demo postgres still works. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- run_confirmed: only create MemoryTool when ConfirmRequest.use_memory is true. Adds use_memory: bool = False to ConfirmRequest. Previously the unconditional _create_memory_tool would ImportError on graphiti_core for SDK installs without the [server] extra (the very thing the lazy import was supposed to avoid). - api.index.main: catch ImportError on uvicorn and exit with a clear message pointing at pip install queryweaver[server], so the queryweaver console-script entrypoint fails gracefully when run from the minimal SDK install. - queryweaver_sdk/__init__.py: trailing whitespace cleanup (after "SchemaResult" and a couple of doctring lines). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Railway/production image runs `uvicorn api.index:app` from start.sh, but the SDK split moved fastapi/uvicorn into the [server] optional-dependencies, so plain `uv sync --frozen --no-dev` no longer installs them. Image came up with `/start.sh: line 30: exec: uvicorn: not found` and the start loop spun forever. Adding `--extra server` to both uv sync calls (the deps-only step and the project-install step) restores fastapi, uvicorn, authlib, itsdangerous, python-multipart, jinja2, fastmcp, graphiti-core, snowflake-connector-python, python-dotenv, and aiohttp in the image. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match the import path to the package name. After this, ``pip install queryweaver`` is followed by ``from queryweaver import QueryWeaver`` (the standard Python convention) instead of the awkward ``queryweaver_sdk`` mismatch. git mv preserves history on the four module files. All importers updated by sed: - api/core/text2sql.py, result_models.py, request_models.py - examples/ecommerce_example.py + examples/README.md - tests/test_sdk/conftest.py, test_queryweaver.py - pyproject.toml ([tool.hatch.build.targets.wheel].packages and the sdist include list) - README.md SDK section Verified: pylint 10/10, 14/14 SDK tests pass, fresh-venv install + ``from queryweaver import QueryWeaver`` works, end-to-end example against demo postgres unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Eliminate four conceptual duplications uncovered in code review: - _emit_schema_refresh: replaces ~25 lines duplicated between run_query and run_confirmed; mark_final_response kwarg makes the wire-format divergence between the two paths explicit. - _stream_pipeline: single async-generator wire serializer for both the query and confirm endpoints in routes/graphs.py. - get_database_type_and_loader gains sdk_only=True; _step_detect_db_type and load_database_sync delegate to it. SDK now rejects snowflake URLs with a clean InvalidArgumentError instead of a deferred ImportError. - _resolve_refresh_target: shared namespace + demo-check + URL fetch preamble; refresh_database_schema and refresh_schema_for_sdk preserve their original raise-vs-RefreshResult contracts. - DESTRUCTIVE_OPS is now derived from _DESTRUCTIVE_VERBS so the verb list cannot drift between membership-test and confirmation-message call sites. Net: -161/+167 LoC across 4 files. Pylint 10.00/10; 118 unit tests + 10 SDK fast tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CodeQL re-flagged information-exposure-through-exception alerts after the _stream_pipeline helper extraction, even though the response message is a caller-provided constant (error_msg) and the exception object never flows into the yielded payload. The previous shape (broad-except inline in each route) was equivalent but happened to evade taint analysis because the generator was function-local. Add lgtm[py/stack-trace-exposure] suppressions on the actual sink (the yield in the except clause) and on each route call site that CodeQL identifies as the flow endpoint, with a docstring explaining the invariant. No behavior change. Closes CodeQL alerts #245, #246. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ection Address review feedback from a follow-up Copilot+CodeQL pass on the dedupe refactor: - routes/graphs: ``run_query`` / ``run_confirmed`` are async generators, so the previous ``try/except InvalidArgumentError`` around the ``StreamingResponse`` was dead code — client errors silently degraded to generic stream events instead of HTTP 400. Validate eagerly (``graph_name``, ``validate_and_truncate_chat``, ``validate_custom_model``, destructive-op preconditions) before constructing the response, and fold the broad-except back into each route's local ``stream()`` so CodeQL's ``py/stack-trace-exposure`` taint analysis stays cleared (the previous ``# lgtm[]`` suppressions did not take effect with GitHub Code Scanning). ``_serialize_pipeline`` keeps the JSON-encoding dedupe but no longer owns exception handling. - core/schema_loader: ``_step_detect_db_type`` now strict-validates the URL scheme against the known set before delegating to ``get_database_type_and_loader``. The unified detector's server-path default-to-PostgreSQL fallback would otherwise misclassify ``sqlite://``/``invalid://`` as PostgreSQL and produce confusing "Detected database type: postgresql" messaging on a deferred connect failure. - api/index: guard ``dotenv``/``api.app_factory`` imports behind ``try/except ImportError`` so ``pip install queryweaver`` (no ``[server]`` extra) can resolve the ``queryweaver`` console script and surface the install-the-extra message instead of an ``ImportError`` at import time. Verified end-to-end in a fresh ``python3.14 -m venv`` install. - queryweaver/client: docstring on ``connect_database`` corrected to name the actual exception (``InvalidArgumentError``, not ``ValueError``). No behavior change for valid inputs. Pylint 10.00/10; 118 unit tests + 10 SDK fast tests pass; fresh-venv SDK install verified. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Supersedes #384. Fixes the correctness bugs surfaced in review, eliminates the
api.extensions.dbglobal mutation, deduplicates orchestration between the streaming and SDK text2sql paths, and makesqueryweaver_sdkstructurally ready forpip install queryweaver. Bumps SDK to 0.2.0 (one breaking change:database_idis now un-prefixed).What changed
Correctness
schema_loader.load_database_syncreturns un-prefixeddatabase_id— fixes double-namespacing when the value is passed back intoquery/delete/get_schema.exceptclause broadened to match streaming path, sopsycopg2.Error/pymysql.Errorfrom bad SQL actually trigger healing.tables_loadedsubstring-counting heuristic (it was double-counting).urllib.parse.urlparsefor DB-name extraction instead of brittlesplit.uv sync --locked --all-extras.Architecture — no more hidden globals
api/core/db_resolver.py::resolve_db()— lazy server-default fallback.api/graph.py(find,get_db_description,get_user_rules,set_user_rules),api/memory/graphiti_tool.py::MemoryTool,api/loaders/{graph,postgres,mysql}_loader.py— all accept optionaldb=Noneparam. Routes unchanged (use resolver fallback, keepfrom api.extensions import db).QueryWeaver.__init__no longer mutatesapi.extensions.db. Multiple SDK instances in one process are now independent.api.extensionsload (no side-effect FalkorDB connect at import time).Deduplication — one text2sql implementation
api/core/text2sql_common.pygained 6 shared helpers:execute_with_healing,refresh_schema_if_modified,quote_identifiers_from_graph,save_memory_background,format_ai_response,build_destructive_confirmation_message.MESSAGE_DELIMITERmoved to the common module (was duplicated in 3 files).text2sql_sync.pyrewritten to call the helpers; streamingtext2sql.pyuses the leaf helpers (inline healing + schema-refresh remain for now — full unification needs queue-based emit, deferred).Layering
api/core/result_models.py+api/core/request_models.py(new) now own the dataclasses.queryweaver_sdk.modelsis a thin re-export shim. Fixes the invertedapi → queryweaver_sdkimport.SDK additions
QueryRequest.custom_api_key/custom_model— per-request LLM overrides (parity with streaming path).QueryWeaver.close()awaits in-flight memory writes via a contextvar-scoped task sink._graph_name, unused_ChatContext.use_user_rules).Tests
tests/test_sdk/test_queryweaver.pyto useasync with QueryWeaver(...)for proper teardown — no more skip-on-RuntimeError wrappers.test_two_instances_isolated(regression guard for the global-mutation fix).test_query_with_history.Breaking changes (SDK 0.1.0 → 0.2.0)
DatabaseConnection.database_idis now the un-prefixed database name (e.g."mydb", not"user_mydb"). Pass this value directly back intoquery,delete_database,get_schema,refresh_schema— the SDK applies namespacing internally.Verification
test_two_instances_isolated). Pylint 10.00/10.uv buildproducesqueryweaver-0.2.0-py3-none-any.whl; installed cleanly in a Python 3.13 fresh venv; import does not loadapi.extensions; two instances have independent_dbhandles.connect_database→get_schema→ natural-language query → SQL (SELECT id, name FROM customers WHERE city = 'New York') → correct filtered results (2 NY customers) →delete_database. All 13 checks passed.uv run uvicorn api.index:app) boots cleanly; 21 routes registered; HSTS middleware active; unchanged behavior from the streaming path's perspective.Test plan
sdk-package-cleanupOPENAI_API_KEY+ PG/MySQL services)🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Tests
Chores