Skip to content

Conversation

@NiveditJain
Copy link
Member

@NiveditJain NiveditJain commented Sep 1, 2025

  • Introduced a new Store model to manage runtime key-value pairs for graph executions, improving state management.
  • Updated GraphTemplate and NodeTemplate models to integrate store configuration, allowing for dynamic input handling during graph execution.
  • Refactored trigger graph functionality to validate required store keys and construct inputs based on node templates.
  • Removed deprecated create_states functionality and replaced it with a more streamlined approach in the trigger_graph controller.
  • Enhanced validation logic in NodeTemplate to prevent reserved keywords as identifiers, ensuring better data integrity.
  • Updated various models and routes to accommodate the new store functionality, improving overall architecture and maintainability.

Pending items:

  • Updating SDK
  • Fixing Tests
  • Adding Documentation

- Introduced a new Store model to manage runtime key-value pairs for graph executions, improving state management.
- Updated GraphTemplate and NodeTemplate models to integrate store configuration, allowing for dynamic input handling during graph execution.
- Refactored trigger graph functionality to validate required store keys and construct inputs based on node templates.
- Removed deprecated create_states functionality and replaced it with a more streamlined approach in the trigger_graph controller.
- Enhanced validation logic in NodeTemplate to prevent reserved keywords as identifiers, ensuring better data integrity.
- Updated various models and routes to accommodate the new store functionality, improving overall architecture and maintainability.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 1, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Warning

Rate limit exceeded

@NiveditJain has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 0 minutes and 58 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 9ade70b and 089a806.

📒 Files selected for processing (2)
  • python-sdk/README.md (2 hunks)
  • python-sdk/exospherehost/statemanager.py (3 hunks)
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Introduces a per-graph Store (beta) to persist key-value data across nodes, with configurable required keys and defaults.
    • Adds store_config to graph templates and SDK upsert to define store behavior.
  • Refactor

    • Simplifies graph triggering: pass inputs and optional store directly; initial state auto-created.
    • Removes the states/create endpoint and legacy state-creation flow.
    • Strengthens validations (identifiers, dependencies, required store keys).
  • Documentation

    • Updated guides and README for new trigger API and Store (beta).
  • Tests

    • Comprehensive updates and additions covering store, validations, and new trigger flow.
  • Chores

    • SDK version bumped to 0.0.2b3.

Walkthrough

Replaces the legacy create_states flow with a store-aware trigger flow: introduces Store and StoreConfig models, extends graph/node validation, registers Store with Beanie, refactors next-state generation to resolve store values asynchronously with caching and batching, removes old create_states endpoint and related models, and updates SDK and docs for the new trigger/store API.

Changes

Cohort / File(s) Summary
Controller: new trigger flow
state-manager/app/controller/trigger_graph.py
New controller and helpers (check_required_store_keys, construct_inputs, trigger_graph) that validate graph/store, persist store entries, create initial root State, and return run_id/status.
Controller: legacy create removed
state-manager/app/controller/create_states.py
Deleted legacy create_states, prior trigger_graph, and get_node_template.
Routes / API wiring
state-manager/app/routes.py
Removed POST /graph/{graph_name}/states/create; swapped imports to models.trigger_model and controller.trigger_graph; trigger route now uses new request/response models.
App startup
state-manager/app/main.py
Registers Store in Beanie document models during app lifespan startup.
DB model: Store added
state-manager/app/models/db/store.py
New Beanie Store document (run_id,namespace,graph_name,key,value), unique composite index, and get_value helper.
GraphTemplate: store config & validators
state-manager/app/models/db/graph_template_model.py
Adds store_config: StoreConfig; trims/normalizes fields; adds validators for unique node identifiers and next_nodes existence; enforces store-field dependency presence in required_keys or default_values.
StoreConfig model
state-manager/app/models/store_config_model.py
New StoreConfig Pydantic model with required_keys and default_values plus validators for trimming, emptiness, '.' prohibition, duplicates, and value normalization.
Dependent string parsing extended
state-manager/app/models/dependent_string.py
Extended placeholder parsing to support store.<key> and existing outputs forms; tightened error handling for invalid formats.
NodeTemplate validation tightened
state-manager/app/models/node_template_model.py
Trim/normalize name/identifier/next_nodes/unites; reject reserved identifier store; ensure non-empty and unique trimmed identifiers.
Trigger models added
state-manager/app/models/trigger_model.py
New TriggerGraphRequestModel (store, inputs) and TriggerGraphResponseModel (status, run_id).
Graph upsert models extended
state-manager/app/models/graph_models.py
Added store_config field to upsert request/response models.
Legacy create models removed
state-manager/app/models/create_models.py
Deleted legacy request/response models for create/trigger flows.
Tasks: create_next_states refactor
state-manager/app/tasks/create_next_states.py
Refactored to async batching and caching: get_store_value (with cache & default fallback), resolves dependencies from store/current/parents, nested async generate_next_state, and batched inserts via asyncio.gather.
Tests: controller updated / deleted
state-manager/tests/unit/controller/test_trigger_graph.py, state-manager/tests/unit/controller/test_create_states.py (deleted)
Trigger tests rewritten for new TriggerGraphRequestModel (store/inputs) and Store/State interactions; legacy create_states tests removed.
Tests: models added/updated
state-manager/tests/unit/models/test_store.py, .../test_store_config_model.py, .../test_dependent_string.py, .../test_node_template_model.py, .../test_graph_template_model.py
New and expanded unit tests for Store, StoreConfig, DependentString, NodeTemplate, and GraphTemplate validators/behaviors.
Tests: init_beanie expectation updated
state-manager/tests/unit/test_main.py
Test updated to expect Store in Beanie document_models.
Tests: tasks create_next_states additions
state-manager/tests/unit/tasks/test_create_next_states.py
Added tests focusing on get_store_value caching, default fallback, isolation, and error propagation.
SDK: interface & docs changes
python-sdk/exospherehost/statemanager.py, python-sdk/exospherehost/_version.py, docs/docs/exosphere/*, python-sdk/README.md
StateManager.trigger now accepts inputs and optional store dicts (replacing TriggerState(s)); upsert_graph adds retry_policy and store_config; SDK version bumped to 0.0.2b3; docs updated to show store (beta) usage.
Python SDK tests updated/added
python-sdk/tests/*
Tests adjusted to new trigger signature and added coverage for runtime, signals, and trigger behaviors; some old input-validation tests removed.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant Routes
  participant Controller as trigger_graph
  participant GT as GraphTemplate(DB)
  participant StoreDB as Store(DB)
  participant StateDB as State(DB)

  Client->>Routes: POST /namespaces/{ns}/graph/{g}/trigger {store, inputs}
  Routes->>Controller: trigger_graph(ns, g, body, req_id)
  Controller->>GT: GraphTemplate.get(ns, g)
  alt not found / invalid
    Controller-->>Routes: HTTP 404/400
    Routes-->>Client: Error
  else valid
    Controller->>Controller: check_required_store_keys(template, body.store)
    Controller->>StoreDB: insert_many(store entries for run_id)
    Controller->>StateDB: insert(root State with merged inputs)
    Controller-->>Routes: {run_id, status=CREATED}
    Routes-->>Client: TriggerGraphResponseModel
  end
Loading
sequenceDiagram
  autonumber
  participant Task as create_next_states
  participant DepStr as DependentString
  participant StoreDB as Store(DB)
  participant GT as GraphTemplate.store_config
  participant Parents as ParentStates
  participant Curr as CurrentState

  Task->>Task: iterate next nodes
  Task->>DepStr: parse dependency token
  alt dependency is store.key
    Task->>StoreDB: get_value(run_id, ns, graph, key)
    alt found
      Task-->>Task: use store value
    else not found
      Task->>GT: check default_values[key]
      alt default exists
        Task-->>Task: use default
      else
        Task-->>Task: raise ValueError (missing store key)
      end
    end
  else dependency is current or parent
    Task->>Curr: current.outputs[field] or Parents: outputs[field]
  end
  Task->>Task: build inputs and return State(status=CREATED)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • nk-ag

Poem

I hid a key in clover brown,
The graph woke up and hopped around.
Run_id glittered under stars,
Stores and defaults fixed the pars.
Hooray — the states all found their ground. 🥕🐇

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • 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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • 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.
  • 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 the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit 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 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

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Summary of Changes

Hello @NiveditJain, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a fundamental change to how runtime data is managed and accessed within graph executions. It centralizes key-value pair storage through a new 'Store' model and significantly refactors the graph triggering mechanism to leverage this new capability. The changes aim to improve the flexibility, maintainability, and data integrity of graph state management.

Highlights

  • New Store Model Introduced: A new 'Store' model has been added to manage runtime key-value pairs for graph executions, significantly improving state management capabilities.
  • Refactored Trigger Graph Functionality: The trigger graph functionality has been completely refactored. It now validates required store keys and constructs inputs dynamically based on node templates, replacing the older 'create_states' approach.
  • Enhanced Graph and Node Template Integration: GraphTemplate and NodeTemplate models have been updated to integrate store configuration, allowing for more dynamic input handling during graph execution. This includes new validation to prevent 'store' from being used as a reserved keyword for node identifiers.
  • Deprecated Create States Functionality Removed: The 'create_states' functionality and its associated models have been removed, streamlining the API and codebase by consolidating state creation into the new trigger graph controller.
  • Dependency Resolution Updated: The logic for resolving input dependencies has been updated to support fetching values from the new 'Store' model, ensuring that graph execution can leverage runtime key-value pairs.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@NiveditJain NiveditJain added enhancement New feature or request do not merge labels Sep 1, 2025
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant and valuable architectural improvement by adding a Store model for managing runtime key-value pairs during graph executions. The refactoring of the graph trigger mechanism to use this new store is well-executed, and the removal of the old create_states API cleans up the codebase. The added validation for node identifiers and store dependencies enhances data integrity.

I've provided a few suggestions to improve the implementation further. These include a critical fix for a bug in the store key validation, along with several recommendations to make the code more concise, idiomatic, and efficient. Overall, these are great changes that improve the maintainability and functionality of the state manager.

@NiveditJain NiveditJain linked an issue Sep 1, 2025 that may be closed by this pull request
@NiveditJain NiveditJain requested a review from nk-ag September 1, 2025 08:37
Copy link
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: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
state-manager/app/main.py (1)

8-8: Import bug: wrong async client symbol; app will fail at startup.

Use PyMongo’s asyncio client module.

-from pymongo import AsyncMongoClient
+from pymongo.asyncio import MongoClient as AsyncMongoClient
state-manager/app/routes.py (1)

76-94: Consider 201 Created for trigger; also dedupe API key checks.

Triggering creates a new run_id; 201 fits better. Repeated API key logging/branching can be centralized in a dependency.

state-manager/app/tasks/create_next_states.py (2)

71-75: validate_dependencies falsely rejects 'store' placeholders

With store support, this validation must allow identifier=="store" and skip the outputs check for it.

-        for dependent in dependency_string.dependents.values():
-            # 2) For each placeholder, verify the identifier is either current or present in parents
-            if dependent.identifier != identifier and dependent.identifier not in parents:
-                raise KeyError(f"Identifier '{dependent.identifier}' not found in parents for template '{next_state_node_template.identifier}'")
+        for dependent in dependency_string.dependents.values():
+            # 2) Allow special "store" identifier; others must be current or present in parents
+            if dependent.identifier == "store":
+                continue
+            if dependent.identifier != identifier and dependent.identifier not in parents:
+                raise KeyError(f"Identifier '{dependent.identifier}' not found in parents for template '{next_state_node_template.identifier}'")
...
-            else:
-                parent_state = parents[dependent.identifier]
-                if dependent.field not in parent_state.outputs:
-                    raise AttributeError(f"Output field '{dependent.field}' not found on state '{dependent.identifier}' for template '{next_state_node_template.identifier}'")
+            else:
+                # Skip output-field validation for "store"; it is resolved at runtime
+                if dependent.identifier != "store":
+                    parent_state = parents[dependent.identifier]
+                    if dependent.field not in parent_state.outputs:
+                        raise AttributeError(f"Output field '{dependent.field}' not found on state '{dependent.identifier}' for template '{next_state_node_template.identifier}'")

Also applies to: 76-84


209-211: Optional: handle duplicate inserts for the non-unites batch too

Mirror the unites path’s DuplicateKeyError/BulkWriteError handling to avoid noisy failures during races.

-        if tasks:
-            new_states = await asyncio.gather(*tasks)
-            await State.insert_many(new_states)
+        if tasks:
+            new_states = await asyncio.gather(*tasks)
+            try:
+                await State.insert_many(new_states)
+            except (DuplicateKeyError, BulkWriteError):
+                logger.warning(
+                    f"Caught duplicate key error for new states in namespace={namespace}, "
+                    f"graph={graph_name}. Attempted to insert {len(new_states)} states"
+                )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between be11f31 and 12b2576.

📒 Files selected for processing (13)
  • state-manager/app/controller/create_states.py (0 hunks)
  • state-manager/app/controller/trigger_graph.py (1 hunks)
  • state-manager/app/main.py (2 hunks)
  • state-manager/app/models/create_models.py (0 hunks)
  • state-manager/app/models/db/graph_template_model.py (3 hunks)
  • state-manager/app/models/db/store.py (1 hunks)
  • state-manager/app/models/dependent_string.py (1 hunks)
  • state-manager/app/models/graph_models.py (1 hunks)
  • state-manager/app/models/node_template_model.py (1 hunks)
  • state-manager/app/models/store_config_model.py (1 hunks)
  • state-manager/app/models/trigger_model.py (1 hunks)
  • state-manager/app/routes.py (1 hunks)
  • state-manager/app/tasks/create_next_states.py (3 hunks)
💤 Files with no reviewable changes (2)
  • state-manager/app/models/create_models.py
  • state-manager/app/controller/create_states.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-09T17:49:27.930Z
Learnt from: NiveditJain
PR: exospherehost/exospherehost#173
File: state-manager/app/tasks/verify_graph.py:57-74
Timestamp: 2025-08-09T17:49:27.930Z
Learning: In the GraphTemplate model in state-manager/app/models/db/graph_template_model.py, the `secrets` field is defined as `Dict[str, str] = Field(default_factory=dict)`, which ensures it's always initialized to at least an empty dictionary and can never be None.

Applied to files:

  • state-manager/app/models/db/graph_template_model.py
🧬 Code graph analysis (8)
state-manager/app/models/trigger_model.py (1)
state-manager/app/models/state_status_enum.py (1)
  • StateStatusEnum (4-13)
state-manager/app/controller/trigger_graph.py (6)
state-manager/app/models/trigger_model.py (2)
  • TriggerGraphRequestModel (4-6)
  • TriggerGraphResponseModel (8-10)
state-manager/app/models/state_status_enum.py (1)
  • StateStatusEnum (4-13)
state-manager/app/models/db/state.py (2)
  • State (13-97)
  • insert_many (55-61)
state-manager/app/models/db/store.py (1)
  • Store (5-36)
state-manager/app/models/db/graph_template_model.py (3)
  • GraphTemplate (17-340)
  • get (313-317)
  • get_root_node (281-285)
state-manager/app/models/node_template_model.py (1)
  • NodeTemplate (17-76)
state-manager/app/models/db/graph_template_model.py (2)
state-manager/app/models/store_config_model.py (1)
  • StoreConfig (3-20)
state-manager/app/models/dependent_string.py (1)
  • get_identifier_field (63-65)
state-manager/app/models/db/store.py (1)
state-manager/app/models/db/graph_template_model.py (1)
  • Settings (32-39)
state-manager/app/main.py (1)
state-manager/app/models/db/store.py (1)
  • Store (5-36)
state-manager/app/models/graph_models.py (1)
state-manager/app/models/store_config_model.py (1)
  • StoreConfig (3-20)
state-manager/app/tasks/create_next_states.py (3)
state-manager/app/models/db/store.py (2)
  • Store (5-36)
  • get_value (27-36)
state-manager/app/models/node_template_model.py (1)
  • NodeTemplate (17-76)
state-manager/app/models/dependent_string.py (5)
  • DependentString (9-65)
  • create_dependent_string (24-45)
  • get_identifier_field (63-65)
  • set_value (57-61)
  • generate_string (14-21)
state-manager/app/routes.py (2)
state-manager/app/models/trigger_model.py (2)
  • TriggerGraphRequestModel (4-6)
  • TriggerGraphResponseModel (8-10)
state-manager/app/controller/trigger_graph.py (1)
  • trigger_graph (41-85)
🔇 Additional comments (8)
state-manager/app/models/graph_models.py (1)

7-7: LGTM: store_config added with safe defaults.

Import and new fields look correct; default_factory ensures backward compatibility for existing clients.

Also applies to: 14-15, 21-21

state-manager/app/models/db/graph_template_model.py (1)

25-25: LGTM: persisted store_config with default_factory.

Keeps templates backward compatible while enabling store-key validation.

state-manager/app/main.py (1)

45-46: Beanie registration with Store looks correct.

No issues spotted here.

state-manager/app/models/trigger_model.py (1)

8-10: Response model LGTM.

state-manager/app/routes.py (1)

12-14: Import rewiring to trigger flow looks good.

state-manager/app/controller/trigger_graph.py (2)

12-12: Logger initialization OK.


65-76: State creation looks correct.

state-manager/app/tasks/create_next_states.py (1)

123-136: Remove unnecessary None-guards for store_config and default_values
GraphTemplate.store_config is declared with Field(default_factory=StoreConfig) and StoreConfig.default_values with Field(default_factory=dict), so neither can ever be None—these extra guards are redundant.

Likely an incorrect or invalid review comment.

- Streamlined the `check_required_store_keys` function to improve validation of required keys in store configurations.
- Enhanced input construction in `construct_inputs` using dictionary comprehensions for better readability.
- Improved error handling in `trigger_graph` to log and raise appropriate HTTP exceptions for missing graph templates.
- Updated `NodeTemplate` and `GraphTemplate` models to include trimming of whitespace in validation, ensuring cleaner input handling.
- Added validation for required keys and default values in `StoreConfig`, enhancing data integrity and error reporting.
- Refactored `create_next_states` to utilize coroutines for state generation, improving performance and clarity in state management.
@NiveditJain
Copy link
Member Author

/gemini review

- Updated the HTTPException detail message in the trigger_graph function to use a consistent string format, enhancing clarity in error reporting.
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant and valuable architectural improvement by adding a Store model for managing runtime key-value pairs, which enhances state management during graph executions. The refactoring of the trigger API, removing the old create_states functionality, results in a cleaner and more streamlined interface. The added validation for identifiers and input stripping are also great additions for data integrity and robustness. My review focuses on improving code clarity, removing redundant checks, and making error handling more robust. The introduction of asyncio.gather for concurrent state creation is a commendable performance enhancement.

- Simplified the validation logic in the GraphTemplate model by removing the check for the "store" identifier in the dependency validation loop, enhancing clarity and reducing unnecessary checks.
- Improved error reporting for dependencies that are not parents of the current node, ensuring more accurate feedback during validation.
Copy link
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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
state-manager/app/tasks/create_next_states.py (1)

203-205: Fix validate_dependencies to allow 'store' placeholders (prevents KeyError).

Store references aren’t in parents; current validation will raise for ${{store.*}}.

-def validate_dependencies(next_state_node_template: NodeTemplate, next_state_input_model: Type[BaseModel], identifier: str, parents: dict[str, State]) -> None:
+def validate_dependencies(next_state_node_template: NodeTemplate, next_state_input_model: Type[BaseModel], identifier: str, parents: dict[str, State]) -> None:
@@
-        for dependent in dependency_string.dependents.values():
-            # 2) For each placeholder, verify the identifier is either current or present in parents
-            if dependent.identifier != identifier and dependent.identifier not in parents:
+        for dependent in dependency_string.dependents.values():
+            # Allow store placeholders; they are resolved via Store and not in parents.
+            if dependent.identifier == "store":
+                continue
+            # 2) For each placeholder, verify the identifier is either current or present in parents
+            if dependent.identifier != identifier and dependent.identifier not in parents:
                 raise KeyError(f"Identifier '{dependent.identifier}' not found in parents for template '{next_state_node_template.identifier}'")
@@
-            else:
-                parent_state = parents[dependent.identifier]
-                if dependent.field not in parent_state.outputs:
-                    raise AttributeError(f"Output field '{dependent.field}' not found on state '{dependent.identifier}' for template '{next_state_node_template.identifier}'")
+            else:
+                parent_state = parents[dependent.identifier]
+                if dependent.field not in parent_state.outputs:
+                    raise AttributeError(f"Output field '{dependent.field}' not found on state '{dependent.identifier}' for template '{next_state_node_template.identifier}'")

Also applies to: 223-225

♻️ Duplicate comments (14)
state-manager/app/models/node_template_model.py (2)

28-31: Simplify empty check (None is redundant).

Use truthiness; Pydantic guarantees non-None for str.

-        if trimmed_v == "" or trimmed_v is None:
+        if not trimmed_v:
             raise ValueError("Node name cannot be empty")

36-41: Harden reserved identifier check (case-insensitive) and simplify emptiness check.

Prevents "Store"/"STORE" slipping through; cleaner condition.

-        if trimmed_v == "" or trimmed_v is None:
+        if not trimmed_v:
             raise ValueError("Node identifier cannot be empty")
-        elif trimmed_v == "store":
-            raise ValueError("Node identifier cannot be reserved word 'store'")
+        elif trimmed_v.lower() == "store":
+            raise ValueError("Node identifier cannot be a reserved keyword 'store'")
         return trimmed_v
state-manager/app/models/db/graph_template_model.py (3)

124-128: Simplify empty check for name.

-        if trimmed_v == "" or trimmed_v is None:
+        if not trimmed_v:
             raise ValueError("Name cannot be empty")

132-135: Simplify empty check for namespace.

-        if trimmed_v == "" or trimmed_v is None:
+        if not trimmed_v:
             raise ValueError("Namespace cannot be empty")

242-263: Make 'store' checks case-insensitive, remove dead branch, and avoid repeated parent lookups.

Also clarifies error message for undeclared store keys.

-                    dependent_identifiers = set()
-                    store_fields = set()
-
-                    for key, field in dependent_string.get_identifier_field():
-                        if key == "store":
-                            store_fields.add(field)
-                        else:
-                            dependent_identifiers.add(key)
-
-                    for identifier in dependent_identifiers:
-                        if identifier == "store":
-                            continue
-
-                        elif identifier not in self.get_parents_by_identifier(node.identifier):
-                            errors.append(f"Input {input_value} depends on {identifier} but {identifier} is not a parent of {node.identifier}")
-
-                    for field in store_fields:
-                        if field not in self.store_config.required_keys and field not in self.store_config.default_values:
-                            errors.append(f"Input {input_value} depends on {field} but {field} is not a required key or a default value")
+                    ident_fields = dependent_string.get_identifier_field()
+                    dependent_identifiers = {identifier for identifier, _ in ident_fields if str(identifier).lower() != "store"}
+                    store_fields = {field for identifier, field in ident_fields if str(identifier).lower() == "store"}
+
+                    parents = self.get_parents_by_identifier(node.identifier)
+                    for identifier in dependent_identifiers:
+                        if identifier not in parents:
+                            errors.append(f"Input {input_value} depends on {identifier} but {identifier} is not a parent of {node.identifier}")
+
+                    for field in store_fields:
+                        if field not in self.store_config.required_keys and field not in self.store_config.default_values:
+                            errors.append(f"Input {input_value} depends on store.{field} but '{field}' is not declared in store_config.required_keys or default_values")
state-manager/app/controller/trigger_graph.py (6)

15-21: Return structured 400 for missing store keys (avoid raw set in message).

Improves client handling and error readability.

-    missing_keys = required_keys - provided_keys
-    if missing_keys:
-        raise HTTPException(status_code=400, detail=f"Missing store keys: {missing_keys}")
+    missing_keys = sorted(required_keys - provided_keys)
+    if missing_keys:
+        raise HTTPException(
+            status_code=400,
+            detail=[f"Missing store key: {k}" for k in missing_keys],
+        )

23-25: Align input types with NodeTemplate (Any).

NodeTemplate.inputs values are Any; keep merging logic unchanged but fix typing.

-def construct_inputs(node: NodeTemplate, inputs: dict[str, str]) -> dict[str, str]:
+def construct_inputs(node: NodeTemplate, inputs: dict[str, Any]) -> dict[str, Any]:
     return {key: inputs.get(key, value) for key, value in node.inputs.items()}

Add import (outside this hunk):

from typing import Any

32-39: Don’t string-match exception messages; map not-found to 404 directly.

Log with stack trace; return clear 404.

-        try:
-            graph_template = await GraphTemplate.get(namespace_name, graph_name)
-        except ValueError as e:
-            logger.error(f"Graph template not found for namespace {namespace_name} and graph {graph_name}", x_exosphere_request_id=x_exosphere_request_id)
-            if "Graph template not found" in str(e):
-                raise HTTPException(status_code=404, detail=f"Graph template not found for namespace {namespace_name} and graph {graph_name}")
-            else:
-                raise e
+        try:
+            graph_template = await GraphTemplate.get(namespace_name, graph_name)
+        except ValueError as e:
+            logger.error(str(e), x_exosphere_request_id=x_exosphere_request_id, exc_info=True)
+            raise HTTPException(status_code=404, detail=str(e)) from e

56-57: Harden store persistence: catch DB errors and avoid leaking internals.

Wrap insert; log with stacktrace; return sanitized 400/500 as appropriate.

-        await Store.insert_many(new_stores)
+        try:
+            await Store.insert_many(new_stores)
+            # consider: ordered=False if supported for throughput
+        except Exception as e:
+            logger.error("Failed inserting store keys", x_exosphere_request_id=x_exosphere_request_id, exc_info=True)
+            raise HTTPException(status_code=400, detail="Failed to persist store entries") from e

56-56: Optional: unordered bulk insert for throughput.

If supported by Beanie/Motor, pass ordered=False.

-        try:
-            await Store.insert_many(new_stores)
+        try:
+            await Store.insert_many(new_stores)  # or: await Store.insert_many(new_stores, ordered=False)

78-80: Avoid re-raising raw exceptions (leaks internals); sanitize to 500.

Preserve HTTPException as-is; log stack traces.

-    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
+    except HTTPException:
+        raise
+    except Exception as e:
+        logger.error(f"Error triggering graph {graph_name} for namespace {namespace_name}", x_exosphere_request_id=x_exosphere_request_id, exc_info=True)
+        raise HTTPException(status_code=500, detail="Internal server error") from e
state-manager/app/tasks/create_next_states.py (3)

206-211: Good fix: coroutines are gathered before insert.

The previous bug is resolved; inserts now receive concrete states.


214-215: Good: unites path also gathers and guards duplicates.

Async batching + duplicate handling is appropriate here.

Also applies to: 231-234


143-156: Rename loop var and add defensive parent-output check.

Avoids shadowing and provides clearer error on missing parent/field.

-            for identifier, field in dependency_string.get_identifier_field():
+            for dep_identifier, field in dependency_string.get_identifier_field():
 
-                    if identifier == "store":
-                        dependency_string.set_value(identifier, field, await get_store_value(current_state.run_id, field))
+                    if dep_identifier == "store":
+                        dependency_string.set_value(dep_identifier, field, await get_store_value(current_state.run_id, field))
 
-                    elif identifier == current_state.identifier:
+                    elif dep_identifier == current_state.identifier:
                         if field not in current_state.outputs:
                             raise AttributeError(f"Output field '{field}' not found on current state '{current_state.identifier}' for template '{next_state_node_template.identifier}'")
-                        dependency_string.set_value(identifier, field, current_state.outputs[field])
+                        dependency_string.set_value(dep_identifier, field, current_state.outputs[field])
                     
                     else:
-                        dependency_string.set_value(identifier, field, parents[identifier].outputs[field])
+                        parent = parents.get(dep_identifier)
+                        if parent is None or field not in parent.outputs:
+                            raise AttributeError(
+                                f"Output field '{field}' not found on parent state '{dep_identifier}' for template '{next_state_node_template.identifier}'"
+                            )
+                        dependency_string.set_value(dep_identifier, field, parent.outputs[field])
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 12b2576 and 815921a.

📒 Files selected for processing (5)
  • state-manager/app/controller/trigger_graph.py (1 hunks)
  • state-manager/app/models/db/graph_template_model.py (4 hunks)
  • state-manager/app/models/node_template_model.py (1 hunks)
  • state-manager/app/models/store_config_model.py (1 hunks)
  • state-manager/app/tasks/create_next_states.py (5 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-09T17:49:27.930Z
Learnt from: NiveditJain
PR: exospherehost/exospherehost#173
File: state-manager/app/tasks/verify_graph.py:57-74
Timestamp: 2025-08-09T17:49:27.930Z
Learning: In the GraphTemplate model in state-manager/app/models/db/graph_template_model.py, the `secrets` field is defined as `Dict[str, str] = Field(default_factory=dict)`, which ensures it's always initialized to at least an empty dictionary and can never be None.

Applied to files:

  • state-manager/app/controller/trigger_graph.py
  • state-manager/app/models/db/graph_template_model.py
🧬 Code graph analysis (3)
state-manager/app/controller/trigger_graph.py (6)
state-manager/app/models/trigger_model.py (2)
  • TriggerGraphRequestModel (4-6)
  • TriggerGraphResponseModel (8-10)
state-manager/app/models/state_status_enum.py (1)
  • StateStatusEnum (4-13)
state-manager/app/models/db/state.py (2)
  • State (13-97)
  • insert_many (55-61)
state-manager/app/models/db/store.py (1)
  • Store (5-36)
state-manager/app/models/db/graph_template_model.py (4)
  • GraphTemplate (17-348)
  • get (321-325)
  • is_valid (286-287)
  • get_root_node (289-293)
state-manager/app/models/node_template_model.py (1)
  • NodeTemplate (17-84)
state-manager/app/models/db/graph_template_model.py (2)
state-manager/app/models/store_config_model.py (1)
  • StoreConfig (3-61)
state-manager/app/models/dependent_string.py (1)
  • get_identifier_field (63-65)
state-manager/app/tasks/create_next_states.py (5)
state-manager/app/models/db/store.py (2)
  • Store (5-36)
  • get_value (27-36)
state-manager/app/models/dependent_string.py (5)
  • DependentString (9-65)
  • create_dependent_string (24-45)
  • get_identifier_field (63-65)
  • set_value (57-61)
  • generate_string (14-21)
state-manager/app/models/node_template_model.py (2)
  • UnitesStrategyEnum (7-9)
  • NodeTemplate (17-84)
state-manager/app/models/db/graph_template_model.py (1)
  • get (321-325)
state-manager/app/models/db/state.py (2)
  • State (13-97)
  • insert_many (55-61)
🔇 Additional comments (1)
state-manager/app/models/store_config_model.py (1)

7-61: StoreConfig validators look solid.

Trim, duplicate, and '.' checks are correct; normalized returns are appropriate.

- Simplified the secrets validation logic by removing unnecessary whitespace trimming, ensuring that the original input is returned after validation.
- Enhanced clarity in the validation process by directly iterating over the input dictionary without modification, while maintaining the integrity of the validation checks.
- Deleted the .coverage file as part of cleanup.
- Updated unit tests to reflect the removal of the create_states route and adjusted assertions accordingly.
- Enhanced TriggerGraphRequestModel validation by incorporating store handling and inputs.
- Refactored test cases to ensure compatibility with the new structure and improved error handling for missing store keys in trigger_graph functionality.
@codecov
Copy link

codecov bot commented Sep 1, 2025

Codecov Report

❌ Patch coverage is 95.65217% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
state-manager/app/tasks/create_next_states.py 88.88% 4 Missing ⚠️
...tate-manager/app/models/db/graph_template_model.py 82.35% 3 Missing ⚠️
python-sdk/exospherehost/statemanager.py 80.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
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: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
state-manager/tests/unit/models/test_store.py (1)

46-65: Parametrize and tighten assertions; avoid re-patching the class each loop.

Improves readability and ensures each case awaits find_one.

-    @pytest.mark.asyncio
-    async def test_get_value_with_different_parameters(self):
-        """Test get_value method with various parameter combinations"""
-        test_cases = [
-            ("run1", "ns1", "graph1", "key1", "value1"),
-            ("run2", "ns2", "graph2", "key2", "value2"),
-            ("", "", "", "", ""),  # Edge case with empty strings
-        ]
-        
-        for run_id, namespace, graph_name, key, expected_value in test_cases:
-            mock_store = MagicMock()
-            mock_store.value = expected_value
-            
-            with patch('app.models.db.store.Store') as mock_store_class:
-                mock_store_class.find_one = AsyncMock(return_value=mock_store)
-                
-                result = await Store.get_value(run_id, namespace, graph_name, key)
-                
-                assert result == expected_value
+    @pytest.mark.asyncio
+    @pytest.mark.parametrize(
+        "run_id,namespace,graph_name,key,expected_value",
+        [
+            ("run1", "ns1", "graph1", "key1", "value1"),
+            ("run2", "ns2", "graph2", "key2", "value2"),
+            ("", "", "", "", ""),  # Edge case with empty strings
+        ],
+    )
+    async def test_get_value_with_different_parameters(self, run_id, namespace, graph_name, key, expected_value):
+        """Test get_value method with various parameter combinations"""
+        mock_store = MagicMock()
+        mock_store.value = expected_value
+        with patch.object(Store, "find_one", new_callable=AsyncMock) as find_one:
+            find_one.return_value = mock_store
+            result = await Store.get_value(run_id, namespace, graph_name, key)
+            assert result == expected_value
+            find_one.assert_awaited()
state-manager/tests/unit/test_routes.py (3)

12-14: Use AsyncMock for async controller patches

If route handlers await controller functions, import AsyncMock so patched callables are awaitable.

-from unittest.mock import MagicMock, patch
+from unittest.mock import MagicMock, AsyncMock, patch

414-429: Make patched trigger_graph awaitable and exercise non-empty payload

Patch with AsyncMock and send a non-empty body to better cover the pass-through.

-    @patch('app.routes.trigger_graph')
+    @patch('app.routes.trigger_graph', new_callable=AsyncMock)
     async def test_trigger_graph_route_with_valid_api_key(self, mock_trigger_graph, mock_request):
@@
-        mock_trigger_graph.return_value = MagicMock()
-        body = TriggerGraphRequestModel()
+        mock_trigger_graph.return_value = MagicMock()
+        body = TriggerGraphRequestModel(store={"s1": "v1"}, inputs={"i1": "v2"})

430-445: Mirror AsyncMock + payload for invalid API key path

Keeps behavior consistent and avoids non-awaitable mocks.

-    @patch('app.routes.trigger_graph')
+    @patch('app.routes.trigger_graph', new_callable=AsyncMock)
     async def test_trigger_graph_route_with_invalid_api_key(self, mock_trigger_graph, mock_request):
@@
-        body = TriggerGraphRequestModel()
+        body = TriggerGraphRequestModel(store={"s1": "v1"}, inputs={"i1": "v2"})
state-manager/tests/unit/models/test_dependent_string.py (1)

71-86: LGTM; add parsing edge cases (whitespace, multi-placeholders, outputs-form).
This will better exercise create_dependent_string.

Append:

+    @pytest.mark.parametrize("syntax, ident, field", [
+        ("x_${{ store.config_key }}_y", "store", "config_key"),  # whitespace trimmed
+        ("a_${{store.k1}}_b_${{store.k2}}_c", "store", "k2"),    # multi-placeholders
+    ])
+    def test_create_dependent_string_parsing_whitespace_and_multiple(self, syntax, ident, field):
+        ds = DependentString.create_dependent_string(syntax)
+        assert ds.dependents  # not empty
+        # last dependent should be accessible and correctly parsed
+        last_idx = max(ds.dependents.keys())
+        d = ds.dependents[last_idx]
+        assert (d.identifier, d.field) == (ident, field)
+
+    def test_create_dependent_string_outputs_form(self):
+        ds = DependentString.create_dependent_string("p_${{node1.outputs.result}}_q")
+        d = ds.dependents[0]
+        assert (d.identifier, d.field, d.tail) == ("node1", "result", "_q")
+
+    @pytest.mark.parametrize("bad", [
+        "p_${{store.key",                # not closed
+        "p_${{node.outputs}}_q",         # missing field
+        "p_${{foo.bar.baz.qux}}_q",      # too many parts
+    ])
+    def test_create_dependent_string_invalid_placeholders(self, bad):
+        with pytest.raises(ValueError):
+            DependentString.create_dependent_string(bad)
+
+    def test_generate_string_with_multiple_placeholders_ordered(self):
+        ds = DependentString.create_dependent_string("A_${{store.k1}}_B_${{store.k2}}_C")
+        ds.set_value("store", "k1", "X")
+        ds.set_value("store", "k2", "Y")
+        assert ds.generate_string() == "A_X_B_Y_C"
state-manager/app/models/db/graph_template_model.py (1)

15-25: StoreConfig integration: OK; add a lightweight backfill plan.

Field with default_factory is safe for new docs; consider a migration to populate store_config on existing documents to avoid surprises during projections/queries.

♻️ Duplicate comments (3)
state-manager/app/models/db/graph_template_model.py (3)

121-128: Simplify None check in name validator.

Apply:

-        trimmed_v = v.strip()
-        if trimmed_v == "" or trimmed_v is None:
+        trimmed_v = v.strip()
+        if not trimmed_v:
             raise ValueError("Name cannot be empty")
         return trimmed_v

129-136: Simplify None check in namespace validator.

Apply:

-        trimmed_v = v.strip()
-        if trimmed_v == "" or trimmed_v is None:
+        trimmed_v = v.strip()
+        if not trimmed_v:
             raise ValueError("Namespace cannot be empty")
         return trimmed_v

241-258: Minor clarity/perf tweaks and clearer store error message.

Apply:

-                    dependent_identifiers = set()
-                    store_fields = set()
-
-                    for key, field in dependent_string.get_identifier_field():
-                        if key == "store":
-                            store_fields.add(field)
-                        else:
-                            dependent_identifiers.add(key)
+                    ident_fields = dependent_string.get_identifier_field()
+                    dependent_identifiers = {k for k, _ in ident_fields if str(k).lower() != "store"}
+                    store_fields = {f for k, f in ident_fields if str(k).lower() == "store"}
+                    parents = self.get_parents_by_identifier(node.identifier)
 
-                    for identifier in dependent_identifiers:
-                        if identifier not in self.get_parents_by_identifier(node.identifier):
+                    for identifier in dependent_identifiers:
+                        if identifier not in parents:
                             errors.append(f"Input {input_value} depends on {identifier} but {identifier} is not a parent of {node.identifier}")
 
-                    for field in store_fields:
-                        if field not in self.store_config.required_keys and field not in self.store_config.default_values:
-                            errors.append(f"Input {input_value} depends on {field} but {field} is not a required key or a default value")
+                    for field in store_fields:
+                        if field not in self.store_config.required_keys and field not in self.store_config.default_values:
+                            errors.append(f"Input {input_value} depends on store.{field} but '{field}' is not declared in store_config.required_keys or default_values")
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between de3f590 and 957cae2.

📒 Files selected for processing (11)
  • state-manager/app/models/db/graph_template_model.py (4 hunks)
  • state-manager/tests/unit/controller/test_create_states.py (0 hunks)
  • state-manager/tests/unit/controller/test_trigger_graph.py (1 hunks)
  • state-manager/tests/unit/models/test_dependent_string.py (1 hunks)
  • state-manager/tests/unit/models/test_graph_template_model.py (2 hunks)
  • state-manager/tests/unit/models/test_node_template_model.py (1 hunks)
  • state-manager/tests/unit/models/test_store.py (1 hunks)
  • state-manager/tests/unit/models/test_store_config_model.py (1 hunks)
  • state-manager/tests/unit/tasks/test_create_next_states.py (1 hunks)
  • state-manager/tests/unit/test_main.py (1 hunks)
  • state-manager/tests/unit/test_routes.py (6 hunks)
💤 Files with no reviewable changes (1)
  • state-manager/tests/unit/controller/test_create_states.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-09T17:49:27.930Z
Learnt from: NiveditJain
PR: exospherehost/exospherehost#173
File: state-manager/app/tasks/verify_graph.py:57-74
Timestamp: 2025-08-09T17:49:27.930Z
Learning: In the GraphTemplate model in state-manager/app/models/db/graph_template_model.py, the `secrets` field is defined as `Dict[str, str] = Field(default_factory=dict)`, which ensures it's always initialized to at least an empty dictionary and can never be None.

Applied to files:

  • state-manager/app/models/db/graph_template_model.py
🧬 Code graph analysis (9)
state-manager/tests/unit/test_main.py (1)
state-manager/app/models/db/store.py (1)
  • Store (5-36)
state-manager/tests/unit/models/test_store.py (1)
state-manager/app/models/db/store.py (1)
  • Store (5-36)
state-manager/tests/unit/models/test_node_template_model.py (2)
state-manager/app/models/node_template_model.py (3)
  • Unites (12-14)
  • UnitesStrategyEnum (7-9)
  • get_dependent_strings (78-84)
state-manager/app/models/dependent_string.py (1)
  • DependentString (9-65)
state-manager/tests/unit/models/test_graph_template_model.py (1)
state-manager/app/models/db/graph_template_model.py (4)
  • GraphTemplate (17-344)
  • get_valid (324-344)
  • is_valid (282-283)
  • is_validating (291-292)
state-manager/tests/unit/models/test_store_config_model.py (1)
state-manager/app/models/store_config_model.py (3)
  • StoreConfig (3-61)
  • validate_required_keys (8-33)
  • validate_default_values (36-61)
state-manager/app/models/db/graph_template_model.py (2)
state-manager/app/models/store_config_model.py (1)
  • StoreConfig (3-61)
state-manager/app/models/dependent_string.py (1)
  • get_identifier_field (63-65)
state-manager/tests/unit/models/test_dependent_string.py (1)
state-manager/app/models/dependent_string.py (7)
  • DependentString (9-65)
  • Dependent (3-7)
  • generate_string (14-21)
  • _build_mapping_key_to_dependent (47-55)
  • set_value (57-61)
  • get_identifier_field (63-65)
  • create_dependent_string (24-45)
state-manager/tests/unit/controller/test_trigger_graph.py (5)
state-manager/app/controller/trigger_graph.py (1)
  • trigger_graph (27-80)
state-manager/app/models/trigger_model.py (1)
  • TriggerGraphRequestModel (4-6)
state-manager/app/models/state_status_enum.py (1)
  • StateStatusEnum (4-13)
state-manager/app/models/db/graph_template_model.py (2)
  • is_valid (282-283)
  • get_root_node (285-289)
state-manager/app/models/db/state.py (1)
  • insert_many (55-61)
state-manager/tests/unit/test_routes.py (1)
state-manager/app/models/trigger_model.py (1)
  • TriggerGraphRequestModel (4-6)
🔇 Additional comments (22)
state-manager/tests/unit/test_main.py (1)

210-210: Good: Store included in Beanie model imports.

Matches the new Store Document registration in app.main.

state-manager/tests/unit/test_routes.py (2)

3-3: Switch to new TriggerGraphRequestModel import — LGTM

Import now targets app.models.trigger_model, matching the refactor.


29-29: Create-states route assertion removed — LGTM

Keeps tests aligned with the deprecated endpoint removal.

state-manager/tests/unit/models/test_dependent_string.py (3)

8-19: LGTM: robust failure-path check.
Good use of match to avoid brittle exact-message coupling.


52-69: LGTM: covers uniqueness of identifier-field pairs.
Assertions are clear and correct.


1-2: Add state-manager to PYTHONPATH in CI
Ensure tests import app.models.dependent_string correctly by exporting PYTHONPATH=.:state-manager (or equivalent) before running pytest.

state-manager/tests/unit/tasks/test_create_next_states.py (1)

636-636: No-op test change is fine.

Trailing newline only; no behavioral impact.

state-manager/tests/unit/models/test_node_template_model.py (4)

9-20: Consider case-insensitive check for reserved identifier "store".

If NodeTemplate validation is case-sensitive, add tests (and impl) for "Store"/"STORE".


35-53: LGTM: get_dependent_strings happy-path coverage.


58-69: LGTM: Unites defaults and explicit strategy tested.


71-78: LGTM: Enum string values asserted.

state-manager/tests/unit/models/test_graph_template_model.py (3)

170-193: LGTM: validates polling_interval/timeout constraints.


228-242: LGTM: non-validating state raises with clear message.


243-259: LGTM: timeout path covered; patching time/sleep is appropriate.

state-manager/tests/unit/models/test_store_config_model.py (4)

8-22: LGTM: default and explicit construction validated.


23-70: LGTM: required_keys validator edge cases comprehensively covered.


82-134: LGTM: default_values validator edge cases comprehensively covered.


135-151: LGTM: integration test ensures end-to-end validation.

state-manager/app/models/db/graph_template_model.py (1)

331-334: LGTM: sensible minimum for polling_interval.

Matches new async tests; prevents tight loops.

state-manager/tests/unit/controller/test_trigger_graph.py (3)

2-2: LGTM: asyncio mocking import is correct.


5-6: LGTM: imports reflect the new trigger_graph API surface.


13-15: LGTM: fixture aligns with TriggerGraphRequestModel (store + inputs).

- Updated `create_graph_template` and `trigger_graph` functions to include optional store configurations for persisting key-value pairs during execution.
- Revised documentation to reflect the new store functionality and its parameters.
- Improved `StateManager` methods to support the new store feature, ensuring better state management across graph executions.
- Introduced a new test suite for StateManager, covering trigger functionality with inputs and validation checks.
- Added tests for Runtime to ensure proper handling of signals, including PruneSignal and ReQueueAfterSignal.
- Implemented mock sessions for testing HTTP interactions and error handling in signal sending.
- Updated existing tests to replace state parameters with inputs for consistency across the codebase.
- Removed outdated test cases from the StateManager test suite to streamline the testing process.
- Added a warning filter to suppress RuntimeWarnings related to coroutines not being awaited in `test_runtime_validation.py`.
- Included a warning filter for Pytest unraisable exceptions in `test_signals_and_runtime_functions.py` to improve test output clarity.
- Introduced a new test class, TestGetStoreValue, to validate the behavior of the get_store_value function.
- Implemented multiple test cases to cover scenarios including cache retrieval, default value fallback, and error handling for missing store values.
- Ensured that the tests verify correct interactions with the Store and proper handling of different run_id and field combinations for cache isolation.
@NiveditJain
Copy link
Member Author

/gemini review

@NiveditJain
Copy link
Member Author

@coderabbitai review this PR

@NiveditJain
Copy link
Member Author

@cursoragent review this pr

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 1, 2025

@NiveditJain I'll conduct a comprehensive review of this PR. This appears to be a significant architectural change introducing the Store model and refactoring the trigger graph functionality. Let me analyze all the changes thoroughly.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant and valuable feature: the Graph Store, which allows persisting key-value pairs across nodes in a graph run. The refactoring of the trigger API to use inputs and store dictionaries instead of the TriggerState object simplifies the SDK and aligns it with the new backend capabilities. The changes are well-implemented across the backend, SDK, documentation, and tests. The new models like Store and StoreConfig are robust with solid validation, and the asynchronous task handling in create_next_states has been nicely optimized using asyncio.gather. The addition of comprehensive unit tests for the new functionality is also a great contribution to the project's quality.

Copy link
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: 24

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (10)
state-manager/tests/unit/tasks/test_create_next_states.py (1)

640-1246: DRY up repeated boilerplate with pytest fixtures/factories.

Almost identical setup (GraphTemplate, NodeTemplates, RegisteredNode, create_model, State) is repeated across tests. Introduce fixtures to improve maintainability and readability.

Example fixtures to add near the top of this file:

import types
import pytest
from unittest.mock import AsyncMock, MagicMock

@pytest.fixture
def registered_node_fixture():
    rn = MagicMock()
    rn.inputs_schema = {}
    return rn

@pytest.fixture
def input_model_fixture():
    m = MagicMock()
    m.model_fields = {}
    return m

@pytest.fixture
def state_query_fixture():
    find = AsyncMock()
    state_cls = MagicMock()
    state_cls.id = "id"
    state_cls.find.return_value = find
    state_cls.insert_many = AsyncMock()
    return state_cls, find

Then reuse these fixtures across tests to cut duplication.

python-sdk/tests/test_runtime_validation.py (1)

61-69: Localize warning suppression and restrict by module

Same here—limit suppression to the specific call sites.

Apply:

-@pytest.mark.filterwarnings("ignore:.*coroutine.*was never awaited.*:RuntimeWarning")
 def test_runtime_invalid_params_raises(monkeypatch):
   monkeypatch.setenv("EXOSPHERE_STATE_MANAGER_URI", "http://sm")
   monkeypatch.setenv("EXOSPHERE_API_KEY", "k")
-  with pytest.raises(ValueError):
-    Runtime(namespace="ns", name="rt", nodes=[GoodNode], batch_size=0)
-  with pytest.raises(ValueError):
-    Runtime(namespace="ns", name="rt", nodes=[GoodNode], workers=0)
+  with warnings.catch_warnings():
+    warnings.filterwarnings(
+      "ignore",
+      message=".*coroutine.*was never awaited.*",
+      category=RuntimeWarning,
+      module=r"exospherehost\..*"
+    )
+    with pytest.raises(ValueError):
+      Runtime(namespace="ns", name="rt", nodes=[GoodNode], batch_size=0)
+    with pytest.raises(ValueError):
+      Runtime(namespace="ns", name="rt", nodes=[GoodNode], workers=0)
python-sdk/tests/test_signals_and_runtime_functions.py (1)

719-729: Test doesn’t actually assert basicConfig usage; patch the correct target

Currently asserting on mock_root_logger.basicConfig can’t catch regressions because logging.basicConfig is module-level. Patch logging.basicConfig and assert not called.

Apply:

-# Mock the root logger to have handlers
-with patch('logging.getLogger') as mock_get_logger:
-    mock_root_logger = MagicMock()
-    mock_root_logger.handlers = [handler]
-    mock_get_logger.return_value = mock_root_logger
-    
-    # This should return early and not configure logging
-    _setup_default_logging()
-    
-    # Verify no basic config was called
-    mock_root_logger.basicConfig = MagicMock()
-    assert not mock_root_logger.basicConfig.called
+with patch('logging.getLogger') as mock_get_logger, \
+     patch('logging.basicConfig') as mock_basic_config:
+    mock_root_logger = MagicMock()
+    mock_root_logger.handlers = [handler]
+    mock_get_logger.return_value = mock_root_logger
+    _setup_default_logging()
+    mock_basic_config.assert_not_called()
docs/docs/exosphere/create-graph.md (1)

178-193: Document full store_config surface (beta) and clarify TTL units

Reflect required_keys/default_values and keep TTL explicit to prevent user confusion.

Apply:

-        # Create or update the graph template (with optional store, beta)
+        # Create or update the graph template (with optional store, beta)
         result = await state_manager.upsert_graph(
@@
-            },
-            store_config={  # beta
-                "ttl": 7200  # seconds to keep key/values
-            }
+            },
+            store_config={  # beta
+                "ttl": 7200,                 # seconds to retain key/values
+                "required_keys": ["user_id"],  # execution-time keys that must be present
+                "default_values": { "region": "us-east-1" }  # optional defaults
+            }

Additionally, consider adding a bullet in “Graph Validation” noting “Identifiers cannot use reserved keywords” to align with NodeTemplate validation.

docs/docs/exosphere/trigger-graph.md (3)

9-11: Remove stale TriggerState import

The new API no longer uses TriggerState in this example; drop it to avoid confusion.

-    from exospherehost import StateManager, TriggerState
+    from exospherehost import StateManager

3-3: Update introductory text to reflect inputs/store trigger API

Replace “sending trigger states to the state manager” with the new call pattern.

-Once you have created a graph template, you can trigger its execution by sending trigger states to the state manager. This guide shows you how to trigger graphs and monitor their execution.
+Once you have created a graph template, you can trigger its execution via StateManager.trigger(...) with inputs and an optional store. This guide shows you how to trigger graphs and monitor their execution.

47-49: Fix typos in “Next Steps”

Capitalize Architecture and fix “unites”.

-- **[ARchitecture](./architecture.md)** - Learn about fanout, unites
+- **[Architecture](./architecture.md)** - Learn about fanout, units
python-sdk/README.md (1)

233-244: Remove unused TriggerState from the example

The example creates a TriggerState but does not use it. Drop it and the import.

-from exospherehost import StateManager, TriggerState
+from exospherehost import StateManager
@@
-# Create a single trigger state
-trigger_state = TriggerState(
-    identifier="user-login",
-    inputs={
-        "user_id": "12345",
-        "session_token": "abc123def456",
-        "timestamp": "2024-01-15T10:30:00Z"
-    }
-)
+# Directly trigger with inputs/store (no TriggerState needed)
python-sdk/exospherehost/statemanager.py (2)

124-158: Mirror timeout/handling in get_graph

Consistent network behavior across methods.

-        async with aiohttp.ClientSession() as session:
-            async with session.get(endpoint, headers=headers) as response: # type: ignore
+        timeout = aiohttp.ClientTimeout(total=15)
+        try:
+            async with aiohttp.ClientSession(timeout=timeout) as session:
+                async with session.get(endpoint, headers=headers) as response:  # type: ignore
                 if response.status != 200:
                     raise Exception(f"Failed to get graph: {response.status} {await response.text()}")
                 return await response.json()
+        except aiohttp.ClientError as e:
+            raise Exception(f"Failed to get graph due to network error: {e}") from e

10-40: Consider deprecating TriggerState in-code

If kept for BC, mark as deprecated in the docstring to steer users to trigger(...).

 class TriggerState(BaseModel):
     """
-    Represents a trigger state for graph execution.
+    Deprecated: Use StateManager.trigger(graph_name, inputs, store) instead.
+
+    Represents a trigger state for graph execution.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a376538 and 9ade70b.

📒 Files selected for processing (11)
  • docs/docs/exosphere/create-graph.md (2 hunks)
  • docs/docs/exosphere/trigger-graph.md (1 hunks)
  • python-sdk/README.md (2 hunks)
  • python-sdk/exospherehost/statemanager.py (2 hunks)
  • python-sdk/tests/test_coverage_additions.py (1 hunks)
  • python-sdk/tests/test_integration.py (2 hunks)
  • python-sdk/tests/test_runtime_validation.py (3 hunks)
  • python-sdk/tests/test_signals_and_runtime_functions.py (3 hunks)
  • python-sdk/tests/test_state_manager.py (0 hunks)
  • python-sdk/tests/test_statemanager_comprehensive.py (3 hunks)
  • state-manager/tests/unit/tasks/test_create_next_states.py (2 hunks)
💤 Files with no reviewable changes (1)
  • python-sdk/tests/test_state_manager.py
🧰 Additional context used
🪛 LanguageTool
python-sdk/README.md

[grammar] ~80-~80: Use correct spacing
Context: ...e pairs across nodes within a single run ## Architecture The SDK is built around tw...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~258-~258: Use correct spacing
Context: ... nodes (beta) } ) ``` Parameters: - graph_name (str): Name of the graph to execute - `i...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~259-~259: Use correct spacing
Context: ...ame(str): Name of the graph to execute -inputs` (dict[str, str] | None): Key/value input...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~260-~260: Use correct spacing
Context: ...inputs for the first node (strings only) - store (dict[str, str] | None): Graph-level key...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~261-~261: Use correct spacing
Context: ...alue store (beta) persisted across nodes Returns: - dict: JSON payload from t...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~263-~263: There might be a mistake here.
Context: ...beta) persisted across nodes Returns: - dict: JSON payload from the state manager *...

(QB_NEW_EN)


[grammar] ~264-~264: Use correct spacing
Context: ...ct: JSON payload from the state manager **Raises:** - Exception`: If the HTTP req...

(QB_NEW_EN_OTHER_ERROR_IDS_5)

🪛 Gitleaks (8.27.2)
python-sdk/README.md

250-250: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🔇 Additional comments (10)
state-manager/tests/unit/tasks/test_create_next_states.py (2)

13-13: LGTM: StoreConfig import aligns tests with new Store-aware flow.


640-1246: Remove Store.get_value verification note. Store.get_value is declared as a @staticmethod in state-manager/app/models/db/store.py and is invoked on the class, so tests patching Store.get_value are valid.

Likely an incorrect or invalid review comment.

python-sdk/tests/test_integration.py (2)

222-224: Good migration to new trigger signature

Using inputs=trigger_state.inputs matches the new API. No further changes needed.


454-455: LGTM: failure path aligned with new trigger signature

The assertion remains correct and uses the updated inputs parameter.

python-sdk/tests/test_statemanager_comprehensive.py (1)

129-131: LGTM: trigger single-state via inputs

Matches the simplified API; assertions remain valid.

python-sdk/tests/test_coverage_additions.py (5)

31-47: Nice coverage for trigger defaults

Asserts empty inputs/store payload; aligns with SDK behavior.


60-77: Good enqueue loop cancellation pattern

Uses CancelledError to stop the task and verifies queue population.


119-136: Prune signal handling test is solid

Patches send and validates dispatch.


138-155: Requeue-after signal handling test is solid

Validates delayed requeue path dispatch.


157-173: Start routine task creation covered well

Cancels cleanly after task creation.

- Revised the README to clarify the Graph Store's functionality, specifying it as a strings-only key-value store with per-run scope.
- Modified the StateManager to conditionally include retry policy and store configuration in the request body, improving flexibility in handling graph execution parameters.
@NiveditJain NiveditJain merged commit ac5b822 into exospherehost:main Sep 1, 2025
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Central store for data that can be persisted through the flow

1 participant