Repository: plastic-labs/honcho
Pull request #94: Add `/all` path to get all apps
Author: login: dr-frmr
Created: 2025-04-02T15:27:53Z | Merged: 2025-04-03T15:57:14Z

Fixes DEV-651


Diff:
diff --git a/src/crud.py b/src/crud.py
index 59821fba8..2424e86c6 100644
--- a/src/crud.py
+++ b/src/crud.py
@@ -51,6 +51,29 @@ async def get_app(db: AsyncSession, app_id: str) -> models.App:
     return app
 
 
+async def get_all_apps(
+    db: AsyncSession,
+    reverse: Optional[bool] = False,
+    filter: Optional[dict] = None,
+) -> Select:
+    """
+    Get all apps.
+
+    Args:
+        db: Database session
+        reverse: Whether to reverse the order of the apps
+        filter: Filter the apps by a dictionary of metadata
+    """
+    stmt = select(models.App)
+    if reverse:
+        stmt = stmt.order_by(models.App.id.desc())
+    else:
+        stmt = stmt.order_by(models.App.id)
+    if filter is not None:
+        stmt = stmt.where(models.App.h_metadata.contains(filter))
+    return stmt
+
+
 async def get_app_by_name(db: AsyncSession, name: str) -> models.App:
     """
     Get an app by its name.
diff --git a/src/routers/apps.py b/src/routers/apps.py
index c3ab4a176..aa180a200 100644
--- a/src/routers/apps.py
+++ b/src/routers/apps.py
@@ -1,6 +1,9 @@
 import logging
+from typing import Optional
 
 from fastapi import APIRouter, Depends
+from fastapi_pagination import Page
+from fastapi_pagination.ext.sqlalchemy import paginate
 
 from src import crud, schemas
 from src.dependencies import db
@@ -31,6 +34,27 @@ async def get_app_from_token(jwt_params=jwt_params, db=db):
     return await crud.get_app(db, app_id=jwt_params.ap)
 
 
+@router.post(
+    "/list",
+    response_model=Page[schemas.App],
+    dependencies=[Depends(require_auth(admin=True))],
+)
+async def get_all_apps(
+    options: schemas.AppGet,
+    reverse: Optional[bool] = False,
+    db=db,
+):
+    """Get all Apps"""
+    return await paginate(
+        db,
+        await crud.get_all_apps(
+            db,
+            reverse=reverse,
+            filter=options.filter,
+        ),
+    )
+
+
 @router.get(
     "/{app_id}",
     response_model=schemas.App,
diff --git a/src/schemas.py b/src/schemas.py
index a2fb23c22..07c3e5b4d 100644
--- a/src/schemas.py
+++ b/src/schemas.py
@@ -13,6 +13,10 @@ class AppCreate(AppBase):
     metadata: dict = {}
 
 
+class AppGet(AppBase):
+    filter: dict | None = None
+
+
 class AppUpdate(AppBase):
     name: str | None = None
     metadata: dict | None = None
@@ -320,24 +324,26 @@ def internal_to_public(cls, value, info):
 class AgentQuery(BaseModel):
     queries: str | list[str]
 
-    @field_validator('queries')
+    @field_validator("queries")
     def validate_queries(cls, v):
         MAX_STRING_LENGTH = 10000
         MAX_LIST_LENGTH = 25
         if isinstance(v, str):
             if len(v) > MAX_STRING_LENGTH:
-                raise ValueError('Query too long')
+                raise ValueError("Query too long")
         elif isinstance(v, list):
             if len(v) > MAX_LIST_LENGTH:
-                raise ValueError('Too many queries')
+                raise ValueError("Too many queries")
             if any(len(q) > MAX_STRING_LENGTH for q in v):
-                raise ValueError('One or more queries too long')
+                raise ValueError("One or more queries too long")
         return v
 
+
 class AgentChat(BaseModel):
     content: str
 
 
 class MessageBatchCreate(BaseModel):
     """Schema for batch message creation with a max of 100 messages"""
+
     messages: list[MessageCreate] = Field(..., max_length=100)
diff --git a/tests/routes/test_apps.py b/tests/routes/test_apps.py
index 1f5463fa2..7c670d5cf 100644
--- a/tests/routes/test_apps.py
+++ b/tests/routes/test_apps.py
@@ -1,3 +1,4 @@
+import pytest
 from nanoid import generate as generate_nanoid
 
 
@@ -80,6 +81,39 @@ def test_get_app_by_id(client, sample_data):
     assert data["id"] == str(test_app.public_id)
 
 
+@pytest.mark.asyncio
+async def test_get_all_apps(client, db_session, sample_data):
+    test_app, test_user = sample_data
+
+    # create a test app with metadata
+    response = client.post(
+        "/v1/apps",
+        json={
+            "name": "test_app",
+            "metadata": {"test_key": "test_value"},
+        },
+    )
+
+    response = client.post(
+        "/v1/apps/list",
+        json={},
+    )
+    assert response.status_code == 200
+    data = response.json()
+    assert "items" in data
+    assert len(data["items"]) > 0
+
+    response = client.post(
+        "/v1/apps/list",
+        json={"filter": {"test_key": "test_value"}},
+    )
+    assert response.status_code == 200
+    data = response.json()
+    assert "items" in data
+    assert len(data["items"]) > 0
+    assert data["items"][0]["metadata"] == {"test_key": "test_value"}
+
+
 def test_get_app_by_name(client, sample_data):
     test_app, _ = sample_data
     response = client.get(f"/v1/apps/name/{test_app.name}")


Reviews:
user: login: VVoruganti
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T14:39:33Z
user: login: VVoruganti
body: Nitpicks for conventions 
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T14:43:39Z
user: login: VVoruganti
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-03T15:57:07Z

Review comments:
diff_hunk: @@ -51,6 +51,15 @@ async def get_app(db: AsyncSession, app_id: str) -> models.App:
     return app
 
 
+async def get_all_apps(db: AsyncSession) -> Sequence[models.App]:
path: src/crud.py
user: login: VVoruganti
body: This should be a paginated method
created_at: 2025-04-03T14:39:32Z
author_association: COLLABORATOR
diff_hunk: @@ -61,6 +61,20 @@ def scaffold_db():
     if table_schema:
         with engine.connect() as connection:
             connection.execute(text(f'CREATE SCHEMA IF NOT EXISTS "{table_schema}"'))
+            # Check if pgvector extension exists in the schema
path: src/db.py
user: login: VVoruganti
body: extensions are not done at a schema level, this should only be necessary once for the database
created_at: 2025-04-03T14:39:54Z
author_association: COLLABORATOR
diff_hunk: @@ -31,6 +31,16 @@ async def get_app_from_token(jwt_params=jwt_params, db=db):
     return await crud.get_app(db, app_id=jwt_params.ap)
 
 
+@router.get(
+    "/all",
path: src/routers/apps.py
user: login: VVoruganti
body: We've been using the `/list` convention for getting multiple. We also generally make them post methods to allow for including metadata based filters. 
created_at: 2025-04-03T14:40:51Z
author_association: COLLABORATOR
diff_hunk: @@ -31,6 +31,16 @@ async def get_app_from_token(jwt_params=jwt_params, db=db):
     return await crud.get_app(db, app_id=jwt_params.ap)
 
 
+@router.get(
+    "/all",
+    response_model=list[schemas.App],
path: src/routers/apps.py
user: login: VVoruganti
body: Should be `Page[schemas.App]`
created_at: 2025-04-03T14:41:09Z
author_association: COLLABORATOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-02T15:29:05Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Auto reviews are disabled on base/target branches other than the default branch.
> 
> 
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=plastic-labs/honcho&utm_content=94):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: dr-frmr
created_at: 2025-04-03T15:30:29Z
body: @VVoruganti Paginated and switched to POST /list
author_association: CONTRIBUTOR