Skip to content

fix: move inline SQLAlchemy queries out of ConversationController into service layer#667

Open
arnavp27 wants to merge 1 commit intopotpie-ai:mainfrom
arnavp27:fix/conversation-store-refactor
Open

fix: move inline SQLAlchemy queries out of ConversationController into service layer#667
arnavp27 wants to merge 1 commit intopotpie-ai:mainfrom
arnavp27:fix/conversation-store-refactor

Conversation

@arnavp27
Copy link
Copy Markdown

@arnavp27 arnavp27 commented Feb 28, 2026

Problem

ConversationController.get_conversations_for_user() contained an inline
select(CustomAgent) query executed directly via self.async_db, bypassing
the store/service layer. Response assembly (UserConversationListResponse)
was also happening in the controller, violating the layered architecture.

Changes

  • conversation_controller.py: Removed inline select(CustomAgent) query
    and unused imports. get_conversations_for_user() now delegates entirely to
    the service (3 lines).

  • conversation_service.py: Added list_conversations_for_user() that
    handles the CustomAgent batch lookup via conversation_store.async_db and
    assembles UserConversationListResponse objects.

Result

All DB reads/writes for the Conversation model now go exclusively through the
store/service layer. Zero inline SQLAlchemy queries remain in the controller.

Summary by CodeRabbit

  • Refactor
    • Improved conversation retrieval system efficiency and backend architecture.

…o service layer

Moves the remaining inline `select(CustomAgent)` query and response assembly
from ConversationController.get_conversations_for_user() into a new service
method ConversationService.list_conversations_for_user(), keeping all DB
access behind the store/service layer and out of the controller.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 28, 2026

Walkthrough

This PR refactors conversation listing by moving aggregation logic from the controller to a new service method list_conversations_for_user. The controller now delegates directly to the service, eliminating inline response construction and per-conversation transformations while maintaining the same data retrieval flow.

Changes

Cohort / File(s) Summary
Controller Simplification
app/modules/conversations/conversation/conversation_controller.py
Removed inline conversation aggregation logic; now delegates directly to service method list_conversations_for_user, eliminating local response object construction and agent/project lookups.
Service Enhancement
app/modules/conversations/conversation/conversation_service.py
Added new public method list_conversations_for_user that retrieves conversations, resolves agent display names via batch CustomAgent queries, builds UserConversationListResponse objects with ISO-formatted timestamps, and handles errors via ConversationServiceError. Introduced imports for SQLAlchemy select, CustomAgentModel, and UserConversationListResponse.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

  • custom-agent-id-fix #317: Both PRs modify conversation agent ID resolution patterns and alter conversation listing logic, with shared changes to conversation_service.py and agent display name handling via CustomAgent lookups.

Poem

🐰 A hop toward cleaner code we go,
From controller clutter, refactored flow,
Service methods now take the stage,
Batch agent lookups fill each page,
Less work above, more sense below! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main objective: moving SQLAlchemy queries from the controller to the service layer, which is exactly what the changeset accomplishes.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

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: 1

🧹 Nitpick comments (1)
app/modules/conversations/conversation/conversation_service.py (1)

1860-1865: Consider routing the CustomAgent lookup through its own store/service layer.

The PR objective is to remove inline SQLAlchemy queries from the controller and route them through the store/service layer. However, this code directly accesses self.conversation_store.async_db to query CustomAgentModel, which:

  1. Bypasses encapsulation of the store layer
  2. Creates a cross-module dependency (conversation service directly querying custom_agents table)

A cleaner approach would be to call self.custom_agent_service (already injected via __init__) to resolve agent display names, or introduce a batch lookup method in CustomAgentService.

♻️ Suggested approach using custom_agent_service
-            if agent_ids:
-                stmt = select(CustomAgentModel).where(
-                    CustomAgentModel.id.in_(agent_ids)
-                )
-                result = await self.conversation_store.async_db.execute(stmt)
-                agents = result.scalars().all()
-                custom_agent_display_names = {agent.id: agent.role for agent in agents}
+            if agent_ids:
+                # Delegate to custom_agent_service for batch lookup
+                custom_agent_display_names = await self.custom_agent_service.get_agent_display_names_batch(agent_ids)

This would require adding a get_agent_display_names_batch method to CustomAgentService.

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

In `@app/modules/conversations/conversation/conversation_service.py` around lines
1860 - 1865, This code bypasses the store layer by directly using
self.conversation_store.async_db to query CustomAgentModel; replace that inline
SQLAlchemy lookup with a call to the injected self.custom_agent_service: add a
batch lookup method (suggested name get_agent_display_names_batch) on
CustomAgentService that accepts a list of agent_ids and returns a dict mapping
id -> display_name/role, then in this function replace the
select/execute/scalars logic with a single await
self.custom_agent_service.get_agent_display_names_batch(agent_ids) to populate
custom_agent_display_names and remove the direct reference to CustomAgentModel
and async_db.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/modules/conversations/conversation/conversation_service.py`:
- Around line 1883-1884: When serializing timestamps in conversation_service
(the lines using conversation.created_at.isoformat() and
conversation.updated_at.isoformat()), guard against None to avoid AttributeError
by checking the attributes before calling isoformat; e.g., replace direct calls
with a conditional that yields None (or a safe default string) when
conversation.created_at or conversation.updated_at is falsy, or extract this
logic into a small helper (e.g., format_ts(ts)) and use it for both created_at
and updated_at to keep the code robust.

---

Nitpick comments:
In `@app/modules/conversations/conversation/conversation_service.py`:
- Around line 1860-1865: This code bypasses the store layer by directly using
self.conversation_store.async_db to query CustomAgentModel; replace that inline
SQLAlchemy lookup with a call to the injected self.custom_agent_service: add a
batch lookup method (suggested name get_agent_display_names_batch) on
CustomAgentService that accepts a list of agent_ids and returns a dict mapping
id -> display_name/role, then in this function replace the
select/execute/scalars logic with a single await
self.custom_agent_service.get_agent_display_names_batch(agent_ids) to populate
custom_agent_display_names and remove the direct reference to CustomAgentModel
and async_db.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2f38075 and 0ea1d4a.

📒 Files selected for processing (2)
  • app/modules/conversations/conversation/conversation_controller.py
  • app/modules/conversations/conversation/conversation_service.py

Comment on lines +1883 to +1884
created_at=conversation.created_at.isoformat(),
updated_at=conversation.updated_at.isoformat(),
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 | 🟡 Minor

Potential AttributeError if timestamps are None.

If conversation.created_at or conversation.updated_at is None, calling .isoformat() will raise an AttributeError. Based on the model definition, these fields have defaults and are marked nullable=False, so this is unlikely in practice—but consider adding a defensive check or using a helper function for robustness.

🛡️ Optional defensive approach
-                        created_at=conversation.created_at.isoformat(),
-                        updated_at=conversation.updated_at.isoformat(),
+                        created_at=conversation.created_at.isoformat() if conversation.created_at else "",
+                        updated_at=conversation.updated_at.isoformat() if conversation.updated_at else "",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/modules/conversations/conversation/conversation_service.py` around lines
1883 - 1884, When serializing timestamps in conversation_service (the lines
using conversation.created_at.isoformat() and
conversation.updated_at.isoformat()), guard against None to avoid AttributeError
by checking the attributes before calling isoformat; e.g., replace direct calls
with a conditional that yields None (or a safe default string) when
conversation.created_at or conversation.updated_at is falsy, or extract this
logic into a small helper (e.g., format_ts(ts)) and use it for both created_at
and updated_at to keep the code robust.

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.

1 participant