Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Binary file added .coverage
Binary file not shown.
67 changes: 67 additions & 0 deletions .github/workflows/test-state-manager.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: State Manager Unit Tests

on:
push:
branches: [main]
paths:
- 'state-manager/**'
pull_request:
branches: [main]
paths:
- 'state-manager/**'

jobs:
test:
runs-on: ubuntu-latest
services:
mongodb:
image: mongo:7
ports:
- 27017:27017
options: >-
--health-cmd "mongosh --eval 'db.runCommand(\"ping\")'"
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install uv
uses: astral-sh/setup-uv@v2
with:
cache: true

- name: Install dev dependencies with uv
working-directory: state-manager
run: |
uv sync --group dev

- name: Run unit tests with pytest and coverage
working-directory: state-manager
run: |
uv run pytest tests/unit/ --cov=app --cov-report=xml --cov-report=term-missing -v --junitxml=pytest-report.xml

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: exospherehost/exospherehost
files: state-manager/coverage.xml
flags: state-manager-unittests
name: state-manager-coverage-report
fail_ci_if_error: true

- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: state-manager-test-results
path: state-manager/pytest-report.xml
retention-days: 30
7 changes: 6 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,10 @@
"tag:yaml.org,2002:python/object/apply:pymdownx.slugs.slugify mapping"
],
"python.analysis.typeCheckingMode": "basic",
"python.analysis.autoImportCompletions": true
"python.analysis.autoImportCompletions": true,
"python.testing.pytestArgs": [
"state-manager"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
37 changes: 33 additions & 4 deletions state-manager/app/controller/create_states.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from fastapi import HTTPException

from app.singletons.logs_manager import LogsManager
from app.models.create_models import CreateRequestModel, CreateResponseModel, ResponseStateModel
from app.models.create_models import CreateRequestModel, CreateResponseModel, ResponseStateModel, TriggerGraphRequestModel, TriggerGraphResponseModel
from app.models.state_status_enum import StateStatusEnum
from app.models.db.state import State
from app.models.db.graph_template_model import GraphTemplate
from app.models.node_template_model import NodeTemplate

from beanie.operators import In
from bson import ObjectId
from beanie import PydanticObjectId
import uuid

logger = LogsManager().get_logger()

Expand All @@ -20,6 +21,33 @@ def get_node_template(graph_template: GraphTemplate, identifier: str) -> NodeTem
return node


async def trigger_graph(namespace_name: str, graph_name: str, body: TriggerGraphRequestModel, x_exosphere_request_id: str) -> TriggerGraphResponseModel:
try:
# Generate a new run ID for this graph execution
run_id = str(uuid.uuid4())
logger.info(f"Triggering graph {graph_name} with run_id {run_id}", x_exosphere_request_id=x_exosphere_request_id)

# Create a CreateRequestModel with the generated run_id
create_request = CreateRequestModel(
run_id=run_id,
states=body.states
)

# Call the existing create_states function
create_response = await create_states(namespace_name, graph_name, create_request, x_exosphere_request_id)

# Return the trigger response with the generated run_id
return TriggerGraphResponseModel(
run_id=run_id,
status=create_response.status,
states=create_response.states
)

except Exception as e:
logger.error(f"Error triggering graph {graph_name} for namespace {namespace_name}", x_exosphere_request_id=x_exosphere_request_id)
raise e


async def create_states(namespace_name: str, graph_name: str, body: CreateRequestModel, x_exosphere_request_id: str) -> CreateResponseModel:
try:
states = []
Expand All @@ -39,6 +67,7 @@ async def create_states(namespace_name: str, graph_name: str, body: CreateReques
node_name=node_template.node_name,
namespace_name=node_template.namespace,
graph_name=graph_name,
run_id=body.run_id,
status=StateStatusEnum.CREATED,
inputs=state.inputs,
outputs={},
Expand All @@ -51,12 +80,12 @@ async def create_states(namespace_name: str, graph_name: str, body: CreateReques
logger.info(f"Created states: {inserted_states.inserted_ids}", x_exosphere_request_id=x_exosphere_request_id)

newStates = await State.find(
In(State.id, [ObjectId(id) for id in inserted_states.inserted_ids])
In(State.id, [PydanticObjectId(id) for id in inserted_states.inserted_ids])
).to_list()

return CreateResponseModel(
status=StateStatusEnum.CREATED,
states=[ResponseStateModel(state_id=str(state.id), identifier=state.identifier, node_name=state.node_name, graph_name=state.graph_name, inputs=state.inputs, created_at=state.created_at) for state in newStates]
states=[ResponseStateModel(state_id=str(state.id), identifier=state.identifier, node_name=state.node_name, graph_name=state.graph_name, run_id=state.run_id, inputs=state.inputs, created_at=state.created_at) for state in newStates]
)
Comment on lines 86 to 89
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Maintain variable naming consistency (snake_case)

Use new_states in the response builder.

-        return CreateResponseModel(
-            status=StateStatusEnum.CREATED,
-            states=[ResponseStateModel(state_id=str(state.id), identifier=state.identifier, node_name=state.node_name, graph_name=state.graph_name, run_id=state.run_id, inputs=state.inputs, created_at=state.created_at) for state in newStates]
-        )
+        return CreateResponseModel(
+            status=StateStatusEnum.CREATED,
+            states=[ResponseStateModel(
+                state_id=str(state.id),
+                identifier=state.identifier,
+                node_name=state.node_name,
+                graph_name=state.graph_name,
+                run_id=state.run_id,
+                inputs=state.inputs,
+                created_at=state.created_at
+            ) for state in new_states]
+        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return CreateResponseModel(
status=StateStatusEnum.CREATED,
states=[ResponseStateModel(state_id=str(state.id), identifier=state.identifier, node_name=state.node_name, graph_name=state.graph_name, inputs=state.inputs, created_at=state.created_at) for state in newStates]
states=[ResponseStateModel(state_id=str(state.id), identifier=state.identifier, node_name=state.node_name, graph_name=state.graph_name, run_id=state.run_id, inputs=state.inputs, created_at=state.created_at) for state in newStates]
)
return CreateResponseModel(
status=StateStatusEnum.CREATED,
states=[ResponseStateModel(
state_id=str(state.id),
identifier=state.identifier,
node_name=state.node_name,
graph_name=state.graph_name,
run_id=state.run_id,
inputs=state.inputs,
created_at=state.created_at
) for state in new_states]
)
🤖 Prompt for AI Agents
In state-manager/app/controller/create_states.py around lines 86 to 89, the
response builder is using camelCase variable newStates; change it to the
snake_case new_states (i.e., replace newStates with new_states in the list
comprehension) so the code uses the existing variable name consistently; update
any other occurrences in this return expression to new_states to avoid NameError
and maintain naming consistency.


except Exception as e:
Expand Down
10 changes: 5 additions & 5 deletions state-manager/app/controller/errored_state.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from app.models.errored_models import ErroredRequestModel, ErroredResponseModel
from bson import ObjectId
from fastapi import HTTPException, status
from beanie import PydanticObjectId

from app.models.db.state import State
from app.models.state_status_enum import StateStatusEnum
from app.singletons.logs_manager import LogsManager

logger = LogsManager().get_logger()

async def errored_state(namespace_name: str, state_id: ObjectId, body: ErroredRequestModel, x_exosphere_request_id: str) -> ErroredResponseModel:
async def errored_state(namespace_name: str, state_id: PydanticObjectId, body: ErroredRequestModel, x_exosphere_request_id: str) -> ErroredResponseModel:

try:
logger.info(f"Errored state {state_id} for namespace {namespace_name}", x_exosphere_request_id=x_exosphere_request_id)
Expand All @@ -23,9 +23,9 @@ async def errored_state(namespace_name: str, state_id: ObjectId, body: ErroredRe
if state.status == StateStatusEnum.EXECUTED:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="State is already executed")

await State.find_one(State.id == state_id).set(
{"status": StateStatusEnum.ERRORED, "error": body.error}
)
state.status = StateStatusEnum.ERRORED
state.error = body.error
await state.save()

return ErroredResponseModel(status=StateStatusEnum.ERRORED)

Expand Down
17 changes: 10 additions & 7 deletions state-manager/app/controller/executed_state.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from beanie import PydanticObjectId
from app.models.executed_models import ExecutedRequestModel, ExecutedResponseModel
from bson import ObjectId

from fastapi import HTTPException, status, BackgroundTasks

from app.models.db.state import State
Expand All @@ -9,7 +10,7 @@

logger = LogsManager().get_logger()

async def executed_state(namespace_name: str, state_id: ObjectId, body: ExecutedRequestModel, x_exosphere_request_id: str, background_tasks: BackgroundTasks) -> ExecutedResponseModel:
async def executed_state(namespace_name: str, state_id: PydanticObjectId, body: ExecutedRequestModel, x_exosphere_request_id: str, background_tasks: BackgroundTasks) -> ExecutedResponseModel:

try:
logger.info(f"Executed state {state_id} for namespace {namespace_name}", x_exosphere_request_id=x_exosphere_request_id)
Expand All @@ -22,17 +23,18 @@ async def executed_state(namespace_name: str, state_id: ObjectId, body: Executed
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="State is not queued")

if len(body.outputs) == 0:
await State.find_one(State.id == state_id).set(
{"status": StateStatusEnum.EXECUTED, "outputs": {}, "parents": {**state.parents, state.identifier: state.id}}
)
state.status = StateStatusEnum.EXECUTED
state.outputs = {}
state.parents = {**state.parents, state.identifier: state.id}
await state.save()

background_tasks.add_task(create_next_state, state)

else:

else:
Comment on lines 25 to +33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Prefer truthy check for outputs and reduce branching duplication

Use a truthy check for readability and consider factoring the common mutation code (parents update + save + scheduling) to avoid duplication.

-        if len(body.outputs) == 0:
+        if not body.outputs:
             state.status = StateStatusEnum.EXECUTED
             state.outputs = {}
             state.parents = {**state.parents, state.identifier: state.id}
             await state.save()

             background_tasks.add_task(create_next_state, state)

-        else:            
+        else:
             state.outputs = body.outputs[0]
             state.status = StateStatusEnum.EXECUTED
             state.parents = {**state.parents, state.identifier: state.id}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if len(body.outputs) == 0:
await State.find_one(State.id == state_id).set(
{"status": StateStatusEnum.EXECUTED, "outputs": {}, "parents": {**state.parents, state.identifier: state.id}}
)
state.status = StateStatusEnum.EXECUTED
state.outputs = {}
state.parents = {**state.parents, state.identifier: state.id}
await state.save()
background_tasks.add_task(create_next_state, state)
else:
else:
if not body.outputs:
state.status = StateStatusEnum.EXECUTED
state.outputs = {}
state.parents = {**state.parents, state.identifier: state.id}
await state.save()
background_tasks.add_task(create_next_state, state)
else:
state.outputs = body.outputs[0]
state.status = StateStatusEnum.EXECUTED
state.parents = {**state.parents, state.identifier: state.id}
🤖 Prompt for AI Agents
In state-manager/app/controller/executed_state.py around lines 25-33, replace
the len(body.outputs) == 0 check with a truthy check (if not body.outputs) and
reduce duplicated mutations: set state.outputs = body.outputs or {} and set
state.status = StateStatusEnum.EXECUTED only when outputs are empty, then always
update state.parents = {**state.parents, state.identifier: state.id} and await
state.save(); finally, keep background_tasks.add_task(create_next_state, state)
inside the branch that runs when outputs are empty so scheduling remains
conditional while parents/save logic is factored out to avoid duplication.

state.outputs = body.outputs[0]
state.status = StateStatusEnum.EXECUTED
state.parents = {**state.parents, state.identifier: state.id}

await state.save()

background_tasks.add_task(create_next_state, state)
Expand All @@ -43,6 +45,7 @@ async def executed_state(namespace_name: str, state_id: ObjectId, body: Executed
namespace_name=state.namespace_name,
identifier=state.identifier,
graph_name=state.graph_name,
run_id=state.run_id,
status=StateStatusEnum.CREATED,
inputs=state.inputs,
outputs=output,
Expand Down
37 changes: 37 additions & 0 deletions state-manager/app/controller/get_current_states.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""
Controller for fetching current states in a namespace
"""
from typing import List

from ..models.db.state import State
from ..singletons.logs_manager import LogsManager


async def get_current_states(namespace: str, request_id: str) -> List[State]:
"""
Get all current states in a namespace

Args:
namespace: The namespace to search in
request_id: Request ID for logging

Returns:
List of all states in the namespace
"""
Comment on lines +10 to +20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Align parameter names with other controllers (namespace_name, x_exosphere_request_id)

Other controllers use namespace_name and x_exosphere_request_id. Rename for consistency and easier grepping across the codebase.

-async def get_current_states(namespace: str, request_id: str) -> List[State]:
+async def get_current_states(namespace_name: str, x_exosphere_request_id: str) -> List[State]:
@@
-        namespace: The namespace to search in
-        request_id: Request ID for logging
+        namespace_name: The namespace to search in
+        x_exosphere_request_id: Request ID for logging
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def get_current_states(namespace: str, request_id: str) -> List[State]:
"""
Get all current states in a namespace
Args:
namespace: The namespace to search in
request_id: Request ID for logging
Returns:
List of all states in the namespace
"""
async def get_current_states(namespace_name: str, x_exosphere_request_id: str) -> List[State]:
"""
Get all current states in a namespace
Args:
namespace_name: The namespace to search in
x_exosphere_request_id: Request ID for logging
Returns:
List of all states in the namespace
"""
🤖 Prompt for AI Agents
In state-manager/app/controller/get_current_states.py around lines 10 to 20, the
function parameter names namespace and request_id should be renamed to
namespace_name and x_exosphere_request_id to match other controllers; update the
async def signature, docstring parameter names, any type hints and all internal
references to use namespace_name and x_exosphere_request_id, and then update any
callers/imports across the codebase to pass the new parameter names (or adjust
keyword usages) so the function and its usages stay consistent.

logger = LogsManager().get_logger()

try:
logger.info(f"Fetching current states for namespace: {namespace}", x_exosphere_request_id=request_id)

# Find all states in the namespace
states = await State.find(
State.namespace_name == namespace
).to_list()
Comment on lines +27 to +29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Potentially unbounded query — consider pagination/limits

This will load all states for a namespace into memory. For large namespaces, this can be slow and memory-heavy. Consider adding optional limit/offset (or a created_after cursor) and a deterministic sort (e.g., by created_at).

Example API shape (outside this hunk):

  • Add optional params: limit: int | None = None, offset: int = 0, sort_desc: bool = True
  • Apply: query = State.find(State.namespace_name == namespace_name); query = query.sort(-State.created_at if sort_desc else State.created_at); if limit is not None: query = query.skip(offset).limit(limit)

💡 Verification agent

🧩 Analysis chain

Ensure there’s an index on State.namespace_name

Filtering by namespace_name without an index can cause full scans. Verify an index exists on this field (via Beanie Indexed or model Settings.indexes). If missing, add one.

If no index is present, consider (in State model):

  • Field-level: namespace_name: Indexed(str)
  • Or Settings: indexes = ['namespace_name']

---

🏁 Script executed:

```shell
#!/bin/bash
# Verify indexing on namespace_name in the State model
set -euo pipefail

echo "Locating State model definition..."
rg -n -C3 -g 'state-manager/app/models/db/state.py' -P '^\s*class\s+State\b'

echo -e "\nSearching for Beanie index hints (Indexed, indexes, IndexModel)..."
rg -n -C3 -g 'state-manager/app/models/db/state.py' -P 'Indexed|indexes|IndexModel|index_models|Settings'

echo -e "\nChecking occurrences of namespace_name field..."
rg -n -C2 -g 'state-manager/app/models/db/state.py' -P '\bnamespace_name\b'

Length of output: 1034


Add a database index on namespace_name in the State model

We didn’t find any existing indexes on namespace_name, so all queries against that field will trigger collection scans. Please add an index to improve query performance.

• File: state-manager/app/models/db/state.py
• Location: around the definition of namespace_name (line ~10)

Suggested changes (choose one):

Option A – Field-level index

-    namespace_name: str = Field(..., description="Name of the namespace of the state")
+    namespace_name: str = Field(..., index=True, description="Name of the namespace of the state")

Option B – Model-level index

+from beanie import IndexModel
+from pymongo import ASCENDING

 class State(BaseDatabaseModel):
     node_name: str = Field(..., description="Name of the node of the state")
     namespace_name: str = Field(..., description="Name of the namespace of the state")
     identifier: str = Field(..., description="Identifier of the node for which state is created")

+    class Settings:
+        indexes = [
+            IndexModel([("namespace_name", ASCENDING)], name="idx_state_namespace")
+        ]

Ensure you import IndexModel and ASCENDING as shown.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
states = await State.find(
State.namespace_name == namespace
).to_list()
++ b/state-manager/app/models/db/state.py
@@ class State(BaseDatabaseModel):
namespace_name: str = Field(
...,
index=True,
description="Name of the namespace of the state"
)
🤖 Prompt for AI Agents
In state-manager/app/models/db/state.py around line ~10, the State model lacks
an index on namespace_name causing collection scans; add an index either by
marking the field with index=True (Field(..., index=True)) or by adding a
model-level index: import IndexModel and ASCENDING from pymongo and add an
IndexModel([("namespace_name", ASCENDING)]) to the model's Meta/indexes list;
ensure the new imports are added at the top of the file.


logger.info(f"Found {len(states)} states for namespace: {namespace}", x_exosphere_request_id=request_id)
Comment on lines +24 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Use structured logging for info logs (avoid f-strings) and align field names

Pass contextual fields as structured args to align with other controllers and improve log parsing/correlation. Also update variable names per the previous comment.

-        logger.info(f"Fetching current states for namespace: {namespace}", x_exosphere_request_id=request_id)
+        logger.info(
+            "Fetching current states for namespace",
+            namespace=namespace_name,
+            x_exosphere_request_id=x_exosphere_request_id,
+        )
@@
-        states = await State.find(
-            State.namespace_name == namespace
-        ).to_list()
+        states = await State.find(
+            State.namespace_name == namespace_name
+        ).to_list()
@@
-        logger.info(f"Found {len(states)} states for namespace: {namespace}", x_exosphere_request_id=request_id)
+        logger.info(
+            "Found states for namespace",
+            namespace=namespace_name,
+            count=len(states),
+            x_exosphere_request_id=x_exosphere_request_id,
+        )

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In state-manager/app/controller/get_current_states.py around lines 24 to 31,
replace the f-string info logs with structured logging by passing contextual
fields as keyword args (e.g. namespace_name, x_exosphere_request_id,
state_count) instead of embedding values in the message; update local variable
names to match the earlier naming convention (use namespace_name if the previous
comment renamed namespace) and log the count of states via a field (state_count
or state_total) rather than interpolating it into the message.


return states

except Exception as e:
logger.error(f"Error fetching current states for namespace {namespace}: {str(e)}", x_exosphere_request_id=request_id)
raise
39 changes: 39 additions & 0 deletions state-manager/app/controller/get_states_by_run_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""
Controller for fetching states by run ID
"""
from typing import List

from ..models.db.state import State
from ..singletons.logs_manager import LogsManager


async def get_states_by_run_id(namespace: str, run_id: str, request_id: str) -> List[State]:
"""
Get all states for a given run ID in a namespace

Args:
namespace: The namespace to search in
run_id: The run ID to filter by
request_id: Request ID for logging

Returns:
List of states for the given run ID
"""
Comment on lines +10 to +21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Align parameter naming with the rest of controllers (namespace_name)

Other controllers consistently use namespace_name. Keeping this consistent reduces cognitive load and avoids mix-ups with namespace fields elsewhere.

Apply:

-async def get_states_by_run_id(namespace: str, run_id: str, request_id: str) -> List[State]:
+async def get_states_by_run_id(namespace_name: str, run_id: str, request_id: str) -> List[State]:
@@
-    Get all states for a given run ID in a namespace
+    Get all states for a given run ID in a namespace
@@
-        namespace: The namespace to search in
+        namespace_name: The namespace to search in
@@
-        logger.info(f"Fetching states for run ID: {run_id} in namespace: {namespace}", x_exosphere_request_id=request_id)
+        logger.info(f"Fetching states for run ID: {run_id} in namespace: {namespace_name}", x_exosphere_request_id=request_id)

And below in the query, replace namespace with namespace_name.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In state-manager/app/controller/get_states_by_run_id.py around lines 10 to 21,
the function parameter is named `namespace` but other controllers use
`namespace_name`; rename the parameter to `namespace_name` (and update its
occurrences in the docstring and function signature) and update the query code
below to replace any `namespace` references with `namespace_name` so naming is
consistent across controllers.

logger = LogsManager().get_logger()

try:
logger.info(f"Fetching states for run ID: {run_id} in namespace: {namespace}", x_exosphere_request_id=request_id)

# Find all states for the run ID in the namespace
states = await State.find(
State.run_id == run_id,
State.namespace_name == namespace
).to_list()
Comment on lines +28 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider indexing and deterministic ordering

  • Add a compound index on (namespace_name, run_id) to speed up this query path.
  • Consider an explicit sort (e.g., by created_at) for deterministic responses.
🤖 Prompt for AI Agents
In state-manager/app/controller/get_states_by_run_id.py around lines 29 to 32,
the query filtering by run_id and namespace lacks a compound index and an
explicit sort; add a compound index on (namespace_name, run_id) in the State
model (or a DB migration/startup index creation) to speed lookups, and change
the query to include a deterministic sort (for example by created_at ascending
or descending) so responses are ordered consistently; ensure the index
definition matches the field names used in the query and that the sort field is
indexed if needed.


logger.info(f"Found {len(states)} states for run ID: {run_id}", x_exosphere_request_id=request_id)

return states

except Exception as e:
logger.error(f"Error fetching states for run ID {run_id} in namespace {namespace}: {str(e)}", x_exosphere_request_id=request_id)
raise
Comment on lines +38 to +39
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Log structured error like other controllers for consistency

Keep logging style consistent across controllers.

-        logger.error(f"Error fetching states for run ID {run_id} in namespace {namespace}: {str(e)}", x_exosphere_request_id=request_id)
+        logger.error(f"Error fetching states for run ID {run_id} in namespace {namespace}", x_exosphere_request_id=request_id, error=e)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
logger.error(f"Error fetching states for run ID {run_id} in namespace {namespace}: {str(e)}", x_exosphere_request_id=request_id)
raise
logger.error(
f"Error fetching states for run ID {run_id} in namespace {namespace}",
x_exosphere_request_id=request_id,
error=e,
)
raise
🤖 Prompt for AI Agents
In state-manager/app/controller/get_states_by_run_id.py around lines 39-40, the
controller currently logs the error using an f-string which is inconsistent with
other controllers; change the logger call to use structured logging like the
others by passing a plain message plus the exception details and request id as
separate keyword fields (e.g., include the exception via exc_info/exception or
an "error" field and pass x_exosphere_request_id=request_id) so the log backend
receives structured keys instead of an interpolated string, then re-raise the
exception.

37 changes: 37 additions & 0 deletions state-manager/app/controller/list_graph_templates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""
Controller for listing graph templates by namespace
"""
from typing import List

from ..models.db.graph_template_model import GraphTemplate
from ..singletons.logs_manager import LogsManager


async def list_graph_templates(namespace: str, request_id: str) -> List[GraphTemplate]:
"""
List all graph templates for a given namespace

Args:
namespace: The namespace to list graph templates for
request_id: Request ID for logging

Returns:
List of graph templates
"""
logger = LogsManager().get_logger()

try:
logger.info(f"Listing graph templates for namespace: {namespace}", x_exosphere_request_id=request_id)

# Find all graph templates for the namespace
templates = await GraphTemplate.find(
GraphTemplate.namespace == namespace
).to_list()
Comment on lines +27 to +29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Ensure DB index on namespace filter

This query will be frequent; ensure GraphTemplate.namespace is indexed to keep the list operation efficient.


🏁 Script executed:

#!/bin/bash
set -e
# Locate the GraphTemplate class definition
rg -n "class GraphTemplate" .

Length of output: 250


🏁 Script executed:

#!/bin/bash
set -e
# Search for namespace field usage and index definitions in GraphTemplate model
rg -n "namespace" state-manager/app/models/db/graph_template_model.py
rg -n "__table_args__" state-manager/app/models/db/graph_template_model.py
rg -n "indexes" state-manager/app/models/db/graph_template_model.py
rg -n "Index" state-manager/app/models/db/graph_template_model.py
# Preview the beginning of the model file to inspect field and meta definitions
sed -n '1,200p' state-manager/app/models/db/graph_template_model.py

Length of output: 331


Add single‐field index on namespace for efficient filtering
The model currently defines only a composite index on (name, namespace), which won’t be used for queries filtering solely by namespace. Since list_graph_templates does frequent GraphTemplate.find(GraphTemplate.namespace == namespace) calls, please add a dedicated index on namespace in the model.

• File: state-manager/app/models/db/graph_template_model.py
• Location: the __table_args__ block beneath the field definitions

Example diff:

 class GraphTemplate(BaseDatabaseModel):
     ...
     __table_args__ = (
-        Index("name", "namespace", unique=True, name="unique_name_namespace"),
+        Index("name", "namespace", unique=True, name="unique_name_namespace"),
+        Index("namespace", name="idx_graph_template_namespace"),
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
templates = await GraphTemplate.find(
GraphTemplate.namespace == namespace
).to_list()
# In state-manager/app/models/db/graph_template_model.py
class GraphTemplate(BaseDatabaseModel):
...
__table_args__ = (
Index("name", "namespace", unique=True, name="unique_name_namespace"),
Index("namespace", name="idx_graph_template_namespace"),
)
🤖 Prompt for AI Agents
In state-manager/app/controller/list_graph_templates.py around lines 27–29 the
code queries GraphTemplate by namespace only; update the model in
state-manager/app/models/db/graph_template_model.py inside the __table_args__
block to add a dedicated single-field index on the namespace column (e.g., add
an Index for namespace alongside the existing composite index), so queries
filtering solely by namespace will use the index; place the new Index entry with
the other table args and run any required migrations to apply the index to the
database.


logger.info(f"Found {len(templates)} graph templates for namespace: {namespace}", x_exosphere_request_id=request_id)

return templates

Comment on lines +27 to +34
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Optional: sort or paginate results

For consistency and predictability, consider returning templates sorted (e.g., by name) and/or add pagination to avoid large result sets.

Happy to provide a patch introducing pagination in the controller and response model.

🤖 Prompt for AI Agents
In state-manager/app/controller/list_graph_templates.py around lines 28 to 35,
the controller currently returns all templates unsorted; update the handler to
accept optional pagination parameters (e.g., limit and offset or page and
page_size) and add a deterministic sort (e.g., GraphTemplate.name ascending) to
the DB query. Modify the GraphTemplate.find(...) chain to
.sort(GraphTemplate.name) and apply .skip(offset).limit(limit) (or equivalent)
before .to_list(), validate and default pagination params, and update the
response model to include pagination metadata (total count, limit, offset) or
ensure the caller can request pages.

except Exception as e:
logger.error(f"Error listing graph templates for namespace {namespace}: {str(e)}", x_exosphere_request_id=request_id)
raise
Comment on lines +35 to +37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Improve error logging (structured + stack trace)

Align with logging style used elsewhere: include structured fields and exc_info=True.

Apply this diff:

-        logger.error(f"Error listing graph templates for namespace {namespace}: {str(e)}", x_exosphere_request_id=request_id)
+        logger.error(
+            "Error listing graph templates",
+            namespace=namespace,
+            x_exosphere_request_id=request_id,
+            error=e,
+            exc_info=True,
+        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
except Exception as e:
logger.error(f"Error listing graph templates for namespace {namespace}: {str(e)}", x_exosphere_request_id=request_id)
raise
except Exception as e:
logger.error(
"Error listing graph templates",
namespace=namespace,
x_exosphere_request_id=request_id,
error=e,
exc_info=True,
)
raise
🤖 Prompt for AI Agents
In state-manager/app/controller/list_graph_templates.py around lines 36 to 38,
the exception handler logs a formatted message string and omits structured
fields and the stack trace; change the logger.error call to use a short static
message and pass namespace and x_exosphere_request_id as structured keyword
fields and include exc_info=True so the stack trace is captured (e.g.
logger.error("Error listing graph templates", namespace=namespace,
x_exosphere_request_id=request_id, exc_info=True)); then re-raise as-is.

37 changes: 37 additions & 0 deletions state-manager/app/controller/list_registered_nodes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""
Controller for listing registered nodes by namespace
"""
from typing import List

from ..models.db.registered_node import RegisteredNode
from ..singletons.logs_manager import LogsManager


async def list_registered_nodes(namespace: str, request_id: str) -> List[RegisteredNode]:
"""
List all registered nodes for a given namespace

Args:
namespace: The namespace to list nodes for
request_id: Request ID for logging

Returns:
List of registered nodes
"""
logger = LogsManager().get_logger()

try:
logger.info(f"Listing registered nodes for namespace: {namespace}", x_exosphere_request_id=request_id)

# Find all registered nodes for the namespace
nodes = await RegisteredNode.find(
RegisteredNode.namespace == namespace
).to_list()
Comment on lines +27 to +29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Ensure DB index on namespace

Add or verify an index on RegisteredNode.namespace to keep list queries fast at scale.


🏁 Script executed:

#!/bin/bash
set -e
echo "Locating RegisteredNode model definition"
rg -n "class RegisteredNode" -C5 || true

echo "Checking for index configuration (e.g., `indexes` attribute)"
rg -n "indexes" -C5 -g "*.py" || true

echo "Searching for `IndexModel` usage"
rg -n "IndexModel" -C5 -g "*.py" || true

Length of output: 5822


🏁 Script executed:

#!/bin/bash
set -e
echo "Locating BaseDatabaseModel definition"
rg -n "class BaseDatabaseModel" -C5 -g "state-manager/app/models/db/base.py" || true

echo "Printing BaseDatabaseModel contents"
sed -n '1,200p' state-manager/app/models/db/base.py || true

Length of output: 1657


Add a DB index on namespace in RegisteredNode

The RegisteredNode model currently has no index defined on namespace, which can lead to slow collection scans as your data grows. Please add a Settings block with a pymongo IndexModel in:

• state-manager/app/models/db/registered_node.py

from pymongo import IndexModel
# … other imports …

class RegisteredNode(BaseDatabaseModel):
    name: str = Field(..., description="Unique name of the registered node")
    namespace: str = Field(..., description="Namespace of the registered node")
    # … other fields …

    class Settings:
        indexes = [
            IndexModel(
                keys=[("namespace", 1)],
                name="idx_registerednode_namespace"
            )
        ]

Also ensure that this index is applied to the existing collection (via your migration or startup logic).

🤖 Prompt for AI Agents
state-manager/app/controller/list_registered_nodes.py lines 27-29: the query
filters RegisteredNode by namespace but the RegisteredNode model lacks a DB
index on namespace leading to full collection scans; update
state-manager/app/models/db/registered_node.py to import pymongo.IndexModel and
add a class Settings with indexes = [IndexModel(keys=[("namespace", 1)],
name="idx_registerednode_namespace")] and then ensure this index is
created/applied at runtime (via an existing migration or startup logic) so the
collection benefits from the new index.


logger.info(f"Found {len(nodes)} registered nodes for namespace: {namespace}", x_exosphere_request_id=request_id)

return nodes

Comment on lines +27 to +34
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider adding pagination/sorting to avoid large payloads

Listing all nodes unbounded can become costly. Consider paginating and/or sorting (e.g., by name) to make responses predictable and scalable.

If you want, I can draft a paginated query pattern using Beanie and extend the response model accordingly.

🤖 Prompt for AI Agents
In state-manager/app/controller/list_registered_nodes.py around lines 28-35, the
handler currently returns all nodes unbounded; add pagination and optional
sorting to avoid large payloads: update the function to accept page (or offset)
and limit (page_size) params with sane defaults and validation, apply a Beanie
query pattern like RegisteredNode.find(RegisteredNode.namespace ==
namespace).sort("name") (or accept sort key/order) then use .skip(page *
limit).limit(limit) (or offset/limit equivalents) to fetch the page, fetch the
total count with .count_documents() to return alongside the items, and extend
the response model to include metadata (total, page, limit) and update the
logger to include pagination info.

except Exception as e:
logger.error(f"Error listing registered nodes for namespace {namespace}: {str(e)}", x_exosphere_request_id=request_id)
raise
Comment on lines +35 to +37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Log exceptions with structured fields and stack trace

Prefer structured error logging with the exception object and stack trace for observability parity with other controllers (e.g., get_graph_template).

Apply this diff:

-        logger.error(f"Error listing registered nodes for namespace {namespace}: {str(e)}", x_exosphere_request_id=request_id)
+        logger.error(
+            "Error listing registered nodes",
+            namespace=namespace,
+            x_exosphere_request_id=request_id,
+            error=e,
+            exc_info=True,
+        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
except Exception as e:
logger.error(f"Error listing registered nodes for namespace {namespace}: {str(e)}", x_exosphere_request_id=request_id)
raise
except Exception as e:
logger.error(
"Error listing registered nodes",
namespace=namespace,
x_exosphere_request_id=request_id,
error=e,
exc_info=True,
)
raise
🤖 Prompt for AI Agents
In state-manager/app/controller/list_registered_nodes.py around lines 36 to 38,
the exception logging currently logs only a formatted string; change it to use
structured logging with the exception and stack trace by calling the logger with
the exception object (e.g., logger.exception or logger.error with exc_info=True)
and include structured fields x_exosphere_request_id=request_id and
namespace=namespace so the stack trace is captured and the fields are available
for observability; keep re-raising the exception unchanged.

Loading
Loading