-
Notifications
You must be signed in to change notification settings - Fork 11
feat: implement Prompt.save() with automatic dirty tracking for name changes #504
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -180,6 +180,9 @@ class Prompt(StateManagementMixin): | |
| prompt.delete() | ||
| """ | ||
|
|
||
| # Fields tracked for dirty-state detection (name changes → DIRTY → save() calls update()) | ||
| _TRACKED_FIELDS: frozenset[str] = frozenset({"name"}) | ||
|
|
||
| # Type annotations for instance attributes | ||
| id: str | None | ||
| name: str | ||
|
|
@@ -203,6 +206,17 @@ def __repr__(self) -> str: | |
| version_info = f", version={self.selected_version_number}" if self.selected_version_number else "" | ||
| return f"Prompt(name='{self.name}', id='{self.id}', messages={len(self.messages)} messages{version_info})" | ||
|
|
||
| def __setattr__(self, attr_name: str, value: Any) -> None: | ||
| """Track mutations to name and transition to DIRTY state when synced.""" | ||
| if attr_name in self._TRACKED_FIELDS and hasattr(self, "_sync_state") and self._sync_state == SyncState.SYNCED: | ||
| try: | ||
| current = object.__getattribute__(self, attr_name) | ||
| except AttributeError: | ||
| current = object() | ||
| if current != value: | ||
| self._set_state(SyncState.DIRTY) | ||
| object.__setattr__(self, attr_name, value) | ||
|
|
||
| def __init__( | ||
| self, | ||
| name: str | None = None, | ||
|
|
@@ -291,7 +305,7 @@ def _update_from_api_response(self, retrieved_prompt: Any) -> None: | |
| retrieved_prompt: The prompt data retrieved from the API. | ||
| """ | ||
| self.id = retrieved_prompt.id | ||
| self.name = retrieved_prompt.name | ||
| object.__setattr__(self, "name", retrieved_prompt.name) | ||
|
|
||
|
Comment on lines
306
to
309
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's wrong: Both Finding type: Want Baz to fix this for you? Activate Fixer |
||
| # Extract messages from the selected_version template using helper | ||
| self.messages = _parse_template_to_messages(retrieved_prompt.selected_version.template) | ||
|
|
@@ -497,8 +511,8 @@ def update(self, *, name: str) -> Prompt: | |
| logger.info(f"Prompt.update: id='{self.id}' name='{name}' - started") | ||
| prompt_service = GlobalPromptTemplates() | ||
| updated_prompt = prompt_service.update(template_id=self.id, name=name) | ||
| # Update our instance attributes | ||
| self.name = updated_prompt.name | ||
| # Update our instance attributes (bypass dirty tracking — this is an internal sync) | ||
| object.__setattr__(self, "name", updated_prompt.name) | ||
| self.updated_at = updated_prompt.updated_at | ||
| # Set state to synced after successful update | ||
| self._set_state(SyncState.SYNCED) | ||
|
|
@@ -764,19 +778,18 @@ def save(self) -> Prompt: | |
| prompt.update(name="renamed-prompt") | ||
| """ | ||
| if self.sync_state == SyncState.LOCAL_ONLY: | ||
| # Prompt hasn't been created yet, create it | ||
| return self.create() | ||
| if self.sync_state == SyncState.SYNCED: | ||
| # Already synced, nothing to do | ||
| logger.debug(f"Prompt.save: id='{self.id}' - already synced, no action needed") | ||
| return self | ||
| if self.sync_state == SyncState.DELETED: | ||
| raise ValueError("Cannot save a deleted prompt.") | ||
| if self.sync_state == SyncState.FAILED_SYNC: | ||
| raise ValueError( | ||
| "Prompt is in FAILED_SYNC state from a previous operation. " | ||
| "Use refresh() to re-sync from the API, or retry the original operation." | ||
| ) | ||
|
|
||
| # DIRTY or FAILED_SYNC states | ||
| # For now, we don't track dirty state for prompts | ||
| # Users should use update() directly for changes | ||
| raise NotImplementedError( | ||
| "Saving modified prompts is not yet implemented. " | ||
| "Use update(name='...') to rename or create_version(messages=[...]) to create a new version." | ||
| ) | ||
| if self.id is None: | ||
| raise ValueError("Prompt ID is not set. Cannot update a prompt without an ID.") | ||
| return self.update(name=self.name) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prompt.setattr flips a synced object's state to DIRTY on any assignment to tracked fields without checking equality, so
prompt.name = prompt.namebecomes DIRTY andsave()(lines 775–788) then callsupdate(name=self.name), issuing an unnecessary API rename. Can we compare the new value to the current one before flipping the sync state so redundant assignments don't trigger updates?Finding type:
Logical Bugs| Severity: 🟠 MediumWant Baz to fix this for you? Activate Fixer
Other fix methods
Prompt for AI Agents:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Commit b1e89cf addressed this comment by checking the current tracked attribute value and only marking the prompt DIRTY if the new value differs, preventing redundant rename updates.