Skip to content
Draft
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
138 changes: 138 additions & 0 deletions execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -1116,6 +1116,144 @@ def get_history(self, prompt_id=None, max_items=None, offset=-1):
else:
return {}

def get_ordered_history(self, max_items=None, offset=0):
"""
Retrieves execution history in chronological order with pagination support.
Returns a lightweight list of history objects.
Used by the /history_v2.

API Output Structure:
{
"history": [
{
"prompt_id": str, # Unique identifier for this execution
"outputs": dict, # Node outputs {node_id: ui_data}
"meta": dict, # Node metadata {node_id: {node_id, display_node, parent_node, real_node_id}}
"prompt": {
"priority": int, # Execution priority
"prompt_id": str, # Same as root prompt_id
"extra_data": dict # Additional metadata (workflow removed from extra_pnginfo)
} | None, # None if no prompt data available
"status": {
"status_str": str, # "success" | "error"
"messages": [ # Filtered execution event messages
(event_name: str, event_data: dict)
]
} | None # None if no status recorded
},
# ... more history items
]
}

Parameters:
- max_items: Maximum number of items to return (None = all)
- offset: Starting index (0-based, negative values calculated from end)
"""
with self.mutex:
history_keys = list(self.history.keys())

if offset < 0 and max_items is not None:
offset = max(0, len(history_keys) - max_items)

end_index = offset + max_items if max_items is not None else None
selected_keys = history_keys[offset:end_index]

history_items = []
for key in selected_keys:
history_entry = self.history[key]

filtered_prompt = None
if "prompt" in history_entry:
priority, prompt_id, _, extra_data, _ = history_entry["prompt"]

filtered_extra_data = {}
for k, v in extra_data.items():
if k == "extra_pnginfo":
filtered_extra_data[k] = {
pk: pv for pk, pv in v.items()
if pk != "workflow"
}
else:
filtered_extra_data[k] = v

filtered_prompt = {
"priority": priority,
"prompt_id": prompt_id,
"extra_data": filtered_extra_data
}

status = None
if history_entry.get("status"):
status = {
"status_str": history_entry["status"]["status_str"],
"messages": [(e, {k: v for k, v in d.items() if k != "nodes"})
if e == "execution_cached" else (e, d)
for e, d in history_entry["status"]["messages"]]
}

item = {
"prompt_id": key,
"outputs": history_entry.get("outputs", {}),
"meta": history_entry.get("meta", {}),
"prompt": filtered_prompt,
"status": status
}

history_items.append(item)

return {"history": history_items}

def get_history_v2(self, prompt_id):
"""
Retrieves execution history for a specific prompt ID.
Used by /history_v2/:prompt_id

API Output Structure:
{
"<prompt_id>": {
"prompt": {
"priority": int, # Execution priority
"prompt_id": str, # Same as the key
"prompt": dict, # The workflow/node data
"extra_data": dict, # Additional metadata (client_id, etc.)
"outputs_to_execute": list # Node IDs to execute
},
"outputs": dict, # Node outputs {node_id: ui_data}
"meta": dict, # Node metadata {node_id: {node_id, display_node, parent_node, real_node_id}}
"status": {
"status_str": str, # "success" | "error"
"completed": bool, # Whether execution finished
"messages": list # Execution event messages
} | None # None if no status recorded
}
}

Returns empty dict {} if prompt_id not found.
"""
with self.mutex:
if prompt_id in self.history:
history_entry = self.history[prompt_id]

new_entry = {}

if "prompt" in history_entry:
priority, prompt_id_inner, prompt_data, extra_data, outputs_to_execute = history_entry["prompt"]
new_entry["prompt"] = {
"priority": priority,
"prompt_id": prompt_id_inner,
"prompt": prompt_data,
"extra_data": extra_data,
"outputs_to_execute": outputs_to_execute
}

for key, value in history_entry.items():
if key != "prompt":
new_entry[key] = value

return {prompt_id: new_entry}
else:
return {}

def wipe_history(self):
with self.mutex:
self.history = {}
Expand Down
16 changes: 16 additions & 0 deletions server.py
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,22 @@ async def get_history_prompt_id(request):
prompt_id = request.match_info.get("prompt_id", None)
return web.json_response(self.prompt_queue.get_history(prompt_id=prompt_id))

@routes.get("/history_v2")
Copy link
Owner

Choose a reason for hiding this comment

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

use a better name like ordered_history

Copy link
Author

Choose a reason for hiding this comment

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

I think the decision was to use this name because we're moving from the old endpoint later on cc: @webfiltered @guill to confirm

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, the intention was to deprecate the /history endpoint eventually in favor of this one (so we don't have to maintain both).

I don't have a particularly strong opinion on the name though. Any of:

  • history_v2
  • history_info
  • workflow_history
  • user_history
  • prompt_history
  • ordered_history
    seem reasonable to me.

async def get_ordered_history(request):
max_items = request.rel_url.query.get("max_items", None)
if max_items is not None:
max_items = int(max_items)

offset = request.rel_url.query.get("offset", 0)
offset = int(offset)

return web.json_response(self.prompt_queue.get_ordered_history(max_items=max_items, offset=offset))

@routes.get("/history_v2/{prompt_id}")
Copy link
Owner

Choose a reason for hiding this comment

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

this is not necessary.

Copy link
Author

Choose a reason for hiding this comment

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

I believe we agreed we'd update this endpoint as well cc: @guill @webfiltered

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, this was specifically my recommendation to avoid a case where people need to remember to use one endpoint for the list and one for individual items (which goes against normal REST principles). It's much easier if we're just able to say "move to this new endpoint as we'll be deprecating /history eventually".

I don't have a strong opinion on the specific name we use, but do strongly feel we should keep this route to preserve consistency.

async def get_history_v2_prompt_id(request):
prompt_id = request.match_info.get("prompt_id", None)
return web.json_response(self.prompt_queue.get_history_v2(prompt_id=prompt_id))

@routes.get("/queue")
async def get_queue(request):
queue_info = {}
Expand Down
Loading
Loading