Files
DemonGatanjieu-Anomalous_Mo…/api/ai_sent.py
T
DemonGatanjieuandClaude Fable 5.2 f600379dac Show what a private task's AI was sent, say what privacy mode cannot cover
Privacy mode asked to be believed: nothing showed the user what had gone
out, and nothing said that it filters Anomalous's answers only.

- ai_sent.py keeps each call of a private task as it went: the arguments
  the AI wrote (tokens) and the answer's text after ai_privacy.guard. A
  line per call beside the task, the newest 300, removed with the task.
- The task's menu opens "What the AI was sent" (ui_ai_sent.js): the calls,
  and a box that looks for a word in all of them.
- ai_callers.reads_disk names the AI apps that read files and run commands
  by themselves. A private task run by one says under its header that such
  an app can open the output folder and the task records by itself; the
  switch's hints and the guide say the same.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 18:53:17 +08:00

72 lines
2.8 KiB
Python

"""What a private task's AI was sent: every tool call as it came in (its arguments, tokens and
all) and the text of the answer as it left, after ai_privacy.guard took out what the task keeps.
Kept beside the task (user/anomalous/ai_sent/<task>/calls.jsonl, gone with it) so the user can
read for themselves what went out and look for a word in it (the task's menu, "What the AI was
sent", GET /anomalous/ai_tasks/{id}/sent). A line per call, the newest MAX_CALLS of them; a long
text is cut at MAX_TEXT with how much was cut. Pictures are counted, not kept: a private task
sends none."""
import json
import os
import threading
import time
from . import ai_tasks
MAX_CALLS = 300
SPARE = 50 # lines over MAX_CALLS before the file is cut back to it
MAX_TEXT = 20_000
MAX_ARGUMENTS = 4_000
_lock = threading.Lock()
_counts = {} # task id -> lines in its file, while this ComfyUI runs
def _path(task_id):
return os.path.join(ai_tasks.marks_dir(task_id, "ai_sent"), "calls.jsonl")
def _cut(text, limit):
return text if len(text) <= limit else f"{text[:limit]}… (+{len(text) - limit})"
def _lines(task_id):
try:
with open(_path(task_id), encoding="utf-8") as source:
return source.readlines()
except OSError:
return []
def keep(task_id, tool, arguments, result):
"""One call of private task `task_id` as its AI had it: what it wrote and what it was answered."""
content = [item for item in (result.get("content") or [] if isinstance(result, dict) else []) if isinstance(item, dict)]
texts = [str(item.get("text") or "") for item in content if item.get("type") == "text"]
line = json.dumps({"at": time.time(), "tool": tool, "arguments": _cut(json.dumps(arguments, ensure_ascii=False), MAX_ARGUMENTS),
"text": _cut("\n".join(texts), MAX_TEXT), "pictures": sum(1 for item in content if item.get("type") == "image"),
"error": bool(isinstance(result, dict) and result.get("isError"))}, ensure_ascii=False) + "\n"
path = _path(task_id)
with _lock:
if task_id not in _counts:
_counts[task_id] = len(_lines(task_id))
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "a", encoding="utf-8") as out:
out.write(line)
_counts[task_id] += 1
if _counts[task_id] > MAX_CALLS + SPARE:
kept = _lines(task_id)[-MAX_CALLS:]
with open(path + ".part", "w", encoding="utf-8") as out:
out.writelines(kept)
os.replace(path + ".part", path)
_counts[task_id] = MAX_CALLS
def calls(task_id):
"""The calls kept for `task_id`, oldest first; [] when it has none."""
found = []
for line in _lines(task_id)[-MAX_CALLS:]:
try:
found.append(json.loads(line))
except ValueError:
continue
return found