mirror of
https://github.com/logseq/logseq.git
synced 2026-05-24 12:44:22 +00:00
enhance(cli): update agent bridge, prompt store in graph
This commit is contained in:
@@ -131,6 +131,27 @@ def run_cli(cli, repo_root, root_dir, config, graph, extra_args):
|
||||
return json.loads(result.stdout) if result.stdout.strip() else None
|
||||
|
||||
|
||||
def run_cli_with_env(cli, repo_root, root_dir, config, extra_args, env):
|
||||
result = subprocess.run(
|
||||
cli
|
||||
+ [
|
||||
"--root-dir",
|
||||
root_dir,
|
||||
"--config",
|
||||
config,
|
||||
"--output",
|
||||
"json",
|
||||
]
|
||||
+ extra_args,
|
||||
cwd=repo_root,
|
||||
env=env,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def deref_session_value(cli, repo_root, root_dir, config, graph, value):
|
||||
if not isinstance(value, int):
|
||||
return value
|
||||
@@ -471,6 +492,200 @@ def run_parallel_assignment_check(cli, repo_root, root_dir, config, graph, tmp_d
|
||||
bridge.wait(timeout=5)
|
||||
|
||||
|
||||
def edn_string(value):
|
||||
return json.dumps(value, ensure_ascii=False)
|
||||
|
||||
|
||||
def write_task_template_file(path, marker):
|
||||
template = "\n".join(
|
||||
[
|
||||
marker,
|
||||
"Graph: {{graph}}",
|
||||
"Block UUID: {{block-uuid}}",
|
||||
"AgentBridge name: {{agent-name}}",
|
||||
"{{task-block-tree}}",
|
||||
]
|
||||
)
|
||||
path.write_text(
|
||||
"""[{:block/title "Task prompt template"
|
||||
:block/children [{:block/title "Description: Custom task template for this graph."}
|
||||
{:block/title "Template variables: {{graph}}, {{block-uuid}}, {{agent-name}}, and {{task-block-tree}}."}
|
||||
{:block/title %s}]}]"""
|
||||
% edn_string("```text\n" + template + "\n```"),
|
||||
encoding="utf8",
|
||||
)
|
||||
|
||||
|
||||
def write_invalid_task_template_file(path):
|
||||
path.write_text(
|
||||
"""[{:block/title "Task prompt template"
|
||||
:block/children [{:block/title "Description: Invalid task template for lint coverage."}
|
||||
{:block/title %s}]}]"""
|
||||
% edn_string("```text\nBroken {{graph}} {{unknown-var}}\n```"),
|
||||
encoding="utf8",
|
||||
)
|
||||
|
||||
|
||||
def install_task_template(cli, repo_root, root_dir, config, graph, tmp_dir, marker):
|
||||
run_cli(
|
||||
cli,
|
||||
repo_root,
|
||||
root_dir,
|
||||
config,
|
||||
graph,
|
||||
["upsert", "page", "--graph", graph, "--page", "AgentBridge"],
|
||||
)
|
||||
blocks_file = tmp_dir / ("task-template-" + graph + ".edn")
|
||||
write_task_template_file(blocks_file, marker)
|
||||
run_cli(
|
||||
cli,
|
||||
repo_root,
|
||||
root_dir,
|
||||
config,
|
||||
graph,
|
||||
[
|
||||
"upsert",
|
||||
"block",
|
||||
"--graph",
|
||||
graph,
|
||||
"--target-page",
|
||||
"AgentBridge",
|
||||
"--blocks-file",
|
||||
str(blocks_file),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def install_invalid_task_template(cli, repo_root, root_dir, config, graph, tmp_dir):
|
||||
run_cli(
|
||||
cli,
|
||||
repo_root,
|
||||
root_dir,
|
||||
config,
|
||||
graph,
|
||||
["upsert", "page", "--graph", graph, "--page", "AgentBridge"],
|
||||
)
|
||||
blocks_file = tmp_dir / ("invalid-task-template-" + graph + ".edn")
|
||||
write_invalid_task_template_file(blocks_file)
|
||||
run_cli(
|
||||
cli,
|
||||
repo_root,
|
||||
root_dir,
|
||||
config,
|
||||
graph,
|
||||
[
|
||||
"upsert",
|
||||
"block",
|
||||
"--graph",
|
||||
graph,
|
||||
"--target-page",
|
||||
"AgentBridge",
|
||||
"--blocks-file",
|
||||
str(blocks_file),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def dry_run_prompt(cli, repo_root, root_dir, config, graph, env):
|
||||
result = run_cli_with_env(
|
||||
cli,
|
||||
repo_root,
|
||||
root_dir,
|
||||
config,
|
||||
["agent", "bridge", "--graph", graph, "--dry-run"],
|
||||
env,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(
|
||||
"agent bridge dry-run failed for {}\nstdout:\n{}\nstderr:\n{}".format(
|
||||
graph, result.stdout, result.stderr
|
||||
)
|
||||
)
|
||||
payload = json.loads(result.stdout)
|
||||
commands = payload.get("data", {}).get("commands", [])
|
||||
if len(commands) != 1:
|
||||
raise SystemExit("expected one dry-run command for {}, got {!r}".format(graph, commands))
|
||||
command = commands[0].get("command", [])
|
||||
if len(command) < 4:
|
||||
raise SystemExit("dry-run command did not contain a prompt: {!r}".format(command))
|
||||
return command[3]
|
||||
|
||||
|
||||
def assert_contains(text, expected):
|
||||
if expected not in text:
|
||||
raise SystemExit("expected {!r} in:\n{}".format(expected, text))
|
||||
|
||||
|
||||
def assert_not_contains(text, unexpected):
|
||||
if unexpected in text:
|
||||
raise SystemExit("did not expect {!r} in:\n{}".format(unexpected, text))
|
||||
|
||||
|
||||
def run_prompt_template_graph_check(cli, repo_root, root_dir, config, graph, tmp_dir):
|
||||
graph_a = graph
|
||||
graph_b = graph + "-other"
|
||||
graph_bad = graph + "-invalid"
|
||||
marker_a = "CUSTOM GRAPH A TEMPLATE"
|
||||
marker_b = "CUSTOM GRAPH B TEMPLATE"
|
||||
|
||||
fake_bin = tmp_dir / "fake-bin"
|
||||
env = os.environ.copy()
|
||||
env["PATH"] = str(fake_bin) + os.pathsep + env.get("PATH", "")
|
||||
env["CODEX_FAKE_LOG"] = str(tmp_dir / "codex-dry-run.jsonl")
|
||||
|
||||
for graph_name in [graph_b, graph_bad]:
|
||||
run_cli(
|
||||
cli,
|
||||
repo_root,
|
||||
root_dir,
|
||||
config,
|
||||
graph_name,
|
||||
["graph", "create", "--graph", graph_name],
|
||||
)
|
||||
|
||||
for graph_name in [graph_a, graph_b, graph_bad]:
|
||||
create_task(cli, repo_root, root_dir, config, graph_name, TASK_TITLE)
|
||||
assign_task(cli, repo_root, root_dir, config, graph_name)
|
||||
|
||||
install_task_template(cli, repo_root, root_dir, config, graph_a, tmp_dir, marker_a)
|
||||
prompt_a = dry_run_prompt(cli, repo_root, root_dir, config, graph_a, env)
|
||||
assert_contains(prompt_a, marker_a)
|
||||
assert_contains(prompt_a, "Graph: " + graph_a)
|
||||
assert_not_contains(prompt_a, marker_b)
|
||||
assert_not_contains(prompt_a, "You are handling a Logseq AgentBridge task.")
|
||||
|
||||
prompt_b_default = dry_run_prompt(cli, repo_root, root_dir, config, graph_b, env)
|
||||
assert_contains(prompt_b_default, "You are handling a Logseq AgentBridge task.")
|
||||
assert_contains(prompt_b_default, "Graph: " + graph_b)
|
||||
assert_not_contains(prompt_b_default, marker_a)
|
||||
|
||||
install_task_template(cli, repo_root, root_dir, config, graph_b, tmp_dir, marker_b)
|
||||
prompt_b = dry_run_prompt(cli, repo_root, root_dir, config, graph_b, env)
|
||||
assert_contains(prompt_b, marker_b)
|
||||
assert_contains(prompt_b, "Graph: " + graph_b)
|
||||
assert_not_contains(prompt_b, marker_a)
|
||||
assert_not_contains(prompt_b, "You are handling a Logseq AgentBridge task.")
|
||||
|
||||
prompt_a_again = dry_run_prompt(cli, repo_root, root_dir, config, graph_a, env)
|
||||
assert_contains(prompt_a_again, marker_a)
|
||||
assert_not_contains(prompt_a_again, marker_b)
|
||||
|
||||
install_invalid_task_template(cli, repo_root, root_dir, config, graph_bad, tmp_dir)
|
||||
bad_result = run_cli_with_env(
|
||||
cli,
|
||||
repo_root,
|
||||
root_dir,
|
||||
config,
|
||||
["agent", "bridge", "--graph", graph_bad, "--dry-run"],
|
||||
env,
|
||||
)
|
||||
if bad_result.returncode == 0:
|
||||
raise SystemExit("invalid graph prompt template unexpectedly passed:\n" + bad_result.stdout)
|
||||
assert_contains(bad_result.stdout + bad_result.stderr, "agent-prompt-template-invalid")
|
||||
|
||||
print("agent bridge graph prompt templates are isolated and linted")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--cli", required=True)
|
||||
@@ -483,6 +698,7 @@ def main():
|
||||
parser.add_argument("--assign-after-start", action="store_true")
|
||||
parser.add_argument("--parallel-assignment-check", action="store_true")
|
||||
parser.add_argument("--comment-mention-check", action="store_true")
|
||||
parser.add_argument("--prompt-template-graph-check", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = pathlib.Path(args.repo_root)
|
||||
@@ -505,6 +721,10 @@ def main():
|
||||
run_comment_mention_check(cli, repo_root, args.root_dir, args.config, args.graph, tmp_dir)
|
||||
return
|
||||
|
||||
if args.prompt_template_graph_check:
|
||||
run_prompt_template_graph_check(cli, repo_root, args.root_dir, args.config, args.graph, tmp_dir)
|
||||
return
|
||||
|
||||
env = os.environ.copy()
|
||||
env["PATH"] = str(fake_bin) + os.pathsep + env.get("PATH", "")
|
||||
env["CODEX_FAKE_LOG"] = str(codex_log)
|
||||
|
||||
@@ -1331,6 +1331,24 @@ PY"
|
||||
"{{cli}} --root-dir '{{tmp-dir}}/parallel-root' --config '{{tmp-dir}}/parallel-root/cli.edn' --output json server stop --graph cli-e2e-agent-bridge-parallel"
|
||||
"{{cli}} --root-dir '{{tmp-dir}}/comment-root' --config '{{tmp-dir}}/comment-root/cli.edn' --output json server stop --graph cli-e2e-agent-bridge-comment"],
|
||||
:tags [:agent]}
|
||||
{:id "agent-bridge-prompt-template-graphs",
|
||||
:setup
|
||||
["mkdir -p '{{tmp-dir}}/prompt-template-root' && printf '{:output-format :json}\\n' > '{{tmp-dir}}/prompt-template-root/cli.edn' && {{cli}} --root-dir '{{tmp-dir}}/prompt-template-root' --config '{{tmp-dir}}/prompt-template-root/cli.edn' --output json graph create --graph cli-e2e-agent-bridge-template-a >/dev/null"],
|
||||
:cmds
|
||||
["python3 '{{repo-root}}/cli-e2e/scripts/agent_bridge_e2e.py' --cli '{{repo-root}}/static/logseq-cli.js' --root-dir '{{tmp-dir}}/prompt-template-root' --config '{{tmp-dir}}/prompt-template-root/cli.edn' --graph cli-e2e-agent-bridge-template-a --tmp-dir '{{tmp-dir}}/prompt-template-work' --repo-root '{{repo-root}}' --prompt-template-graph-check"],
|
||||
:expect
|
||||
{:exit 0,
|
||||
:stdout-contains ["agent bridge graph prompt templates are isolated and linted"]},
|
||||
:covers
|
||||
{:commands ["agent bridge"],
|
||||
:options
|
||||
{:global ["--config" "--graph" "--root-dir" "--output"],
|
||||
:agent ["--dry-run"]}},
|
||||
:cleanup
|
||||
["{{cli}} --root-dir '{{tmp-dir}}/prompt-template-root' --config '{{tmp-dir}}/prompt-template-root/cli.edn' --output json server stop --graph cli-e2e-agent-bridge-template-a"
|
||||
"{{cli}} --root-dir '{{tmp-dir}}/prompt-template-root' --config '{{tmp-dir}}/prompt-template-root/cli.edn' --output json server stop --graph cli-e2e-agent-bridge-template-a-other"
|
||||
"{{cli}} --root-dir '{{tmp-dir}}/prompt-template-root' --config '{{tmp-dir}}/prompt-template-root/cli.edn' --output json server stop --graph cli-e2e-agent-bridge-template-a-invalid"],
|
||||
:tags [:agent]}
|
||||
{:id "skill-show-human",
|
||||
:cmds ["{{cli}} skill show"],
|
||||
:expect
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
["os" :as os]
|
||||
["path" :as node-path]
|
||||
[cljs.reader :as reader]
|
||||
[clojure.set :as set]
|
||||
[clojure.string :as string]
|
||||
[lambdaisland.glogi :as log]
|
||||
[logseq.cli.command.core :as core]
|
||||
@@ -12,6 +13,7 @@
|
||||
[logseq.cli.server :as cli-server]
|
||||
[logseq.cli.transport :as transport]
|
||||
[logseq.common.util :as common-util]
|
||||
[logseq.db :as ldb]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(def ^:private bridge-spec
|
||||
@@ -175,52 +177,157 @@
|
||||
[block]
|
||||
(some-> (:block/uuid block) str))
|
||||
|
||||
(defn build-codex-prompt
|
||||
[{:keys [graph agent-name block tree-text]}]
|
||||
(def task-prompt-template-title "Task prompt template")
|
||||
|
||||
(def comment-prompt-template-title "Comment prompt template")
|
||||
|
||||
(def ^:private default-task-prompt-template
|
||||
(string/join
|
||||
"\n"
|
||||
["You are handling a Logseq AgentBridge task."
|
||||
""
|
||||
(str "Graph: " graph)
|
||||
(str "Block UUID: " (block-uuid-str block))
|
||||
(str "AgentBridge name: " agent-name)
|
||||
"Graph: {{graph}}"
|
||||
"Block UUID: {{block-uuid}}"
|
||||
"AgentBridge name: {{agent-name}}"
|
||||
""
|
||||
"Do not operate outside the target graph."
|
||||
"Write task results back into the graph."
|
||||
"Report the final status, files changed, commands run, verification, and any blockers."
|
||||
""
|
||||
"Task block tree:"
|
||||
(or tree-text (:block/title block) "")]))
|
||||
"{{task-block-tree}}"]))
|
||||
|
||||
(defn- build-comment-codex-prompt
|
||||
[{:keys [graph agent-name comment-tree-text comments-area-tree-text target-tree-texts]
|
||||
comment-block :comment}]
|
||||
(def ^:private default-comment-prompt-template
|
||||
(string/join
|
||||
"\n"
|
||||
["You are handling a Logseq AgentBridge comment request."
|
||||
""
|
||||
(str "Graph: " graph)
|
||||
(str "Comment UUID: " (block-uuid-str comment-block))
|
||||
(str "AgentBridge name: " agent-name)
|
||||
"Graph: {{graph}}"
|
||||
"Comment UUID: {{comment-uuid}}"
|
||||
"AgentBridge name: {{agent-name}}"
|
||||
""
|
||||
"Do not operate outside the target graph."
|
||||
"Complete the request from the mentioned comment."
|
||||
"Report the final status, files changed, commands run, verification, and any blockers."
|
||||
""
|
||||
"Comment target context:"
|
||||
(string/join "\n" (remove string/blank? target-tree-texts))
|
||||
"{{comment-target-context}}"
|
||||
""
|
||||
"Comment thread context:"
|
||||
(or comments-area-tree-text (:block/title (:block/parent comment-block)) "")
|
||||
"{{comment-thread-context}}"
|
||||
""
|
||||
"Requesting comment:"
|
||||
(or comment-tree-text (:block/title comment-block) "")
|
||||
"{{requesting-comment}}"
|
||||
""
|
||||
"Reply instructions:"
|
||||
"For a short reply, append a comment after the requesting comment."
|
||||
"For a long reply, write a normal block tree after the comments area and append a comment that references that tree."
|
||||
"If the request is blocked or fails, make that clear in the reply."]))
|
||||
|
||||
(def ^:private prompt-template-vars
|
||||
{:task #{"graph"
|
||||
"block-uuid"
|
||||
"agent-name"
|
||||
"task-block-tree"}
|
||||
:comment #{"graph"
|
||||
"comment-uuid"
|
||||
"agent-name"
|
||||
"comment-target-context"
|
||||
"comment-thread-context"
|
||||
"requesting-comment"}})
|
||||
|
||||
(def ^:private required-prompt-template-vars prompt-template-vars)
|
||||
|
||||
(defn- renderable-prompt-template-var-names
|
||||
[template]
|
||||
(->> (re-seq #"('?)(\{\{([A-Za-z0-9-]+)\}\})('?)" (or template ""))
|
||||
(keep (fn [[_ open-quote _ var-name close-quote]]
|
||||
(when-not (and (= "'" open-quote)
|
||||
(= "'" close-quote))
|
||||
var-name)))
|
||||
set))
|
||||
|
||||
(defn validate-prompt-template
|
||||
[template-kind template]
|
||||
(let [vars (renderable-prompt-template-var-names template)
|
||||
allowed-vars (get prompt-template-vars template-kind)
|
||||
required-vars (get required-prompt-template-vars template-kind)]
|
||||
(if (nil? allowed-vars)
|
||||
{:ok? false
|
||||
:error {:code :unknown-template-kind
|
||||
:template template-kind}}
|
||||
(let [unknown-vars (set/difference vars allowed-vars)
|
||||
missing-vars (set/difference required-vars vars)]
|
||||
(cond
|
||||
(string/blank? (or template ""))
|
||||
{:ok? false
|
||||
:error {:code :missing-template-code-block
|
||||
:template template-kind}}
|
||||
|
||||
(seq unknown-vars)
|
||||
{:ok? false
|
||||
:error {:code :unknown-template-vars
|
||||
:template template-kind
|
||||
:vars unknown-vars}}
|
||||
|
||||
(seq missing-vars)
|
||||
{:ok? false
|
||||
:error {:code :missing-template-vars
|
||||
:template template-kind
|
||||
:vars missing-vars}}
|
||||
|
||||
:else
|
||||
{:ok? true})))))
|
||||
|
||||
(defn- validate-prompt-templates!
|
||||
[templates]
|
||||
(doseq [[template-kind template] templates]
|
||||
(let [result (validate-prompt-template template-kind template)]
|
||||
(when-not (:ok? result)
|
||||
(throw (ex-info "agent bridge prompt template is invalid"
|
||||
(assoc (:error result)
|
||||
:code :agent-prompt-template-invalid
|
||||
:reason (get-in result [:error :code])))))))
|
||||
templates)
|
||||
|
||||
(defn- render-prompt-template
|
||||
[template-kind template vars]
|
||||
(validate-prompt-templates! {template-kind template})
|
||||
(string/replace
|
||||
template
|
||||
#"\{\{([A-Za-z0-9-]+)\}\}"
|
||||
(fn [[_ var-name]]
|
||||
(if (contains? vars var-name)
|
||||
(str (get vars var-name))
|
||||
(throw (ex-info "agent bridge prompt template var has no value"
|
||||
{:code :agent-prompt-template-var-missing
|
||||
:template template-kind
|
||||
:var var-name}))))))
|
||||
|
||||
(defn build-codex-prompt
|
||||
[{:keys [graph agent-name block tree-text prompt-template]}]
|
||||
(render-prompt-template
|
||||
:task
|
||||
(or prompt-template default-task-prompt-template)
|
||||
{"graph" graph
|
||||
"block-uuid" (block-uuid-str block)
|
||||
"agent-name" agent-name
|
||||
"task-block-tree" (or tree-text (:block/title block) "")}))
|
||||
|
||||
(defn- build-comment-codex-prompt
|
||||
[{:keys [graph agent-name comment-tree-text comments-area-tree-text target-tree-texts]
|
||||
comment-block :comment
|
||||
prompt-template :prompt-template}]
|
||||
(render-prompt-template
|
||||
:comment
|
||||
(or prompt-template default-comment-prompt-template)
|
||||
{"graph" graph
|
||||
"comment-uuid" (block-uuid-str comment-block)
|
||||
"agent-name" agent-name
|
||||
"comment-target-context" (string/join "\n" (remove string/blank? target-tree-texts))
|
||||
"comment-thread-context" (or comments-area-tree-text (:block/title (:block/parent comment-block)) "")
|
||||
"requesting-comment" (or comment-tree-text (:block/title comment-block) "")}))
|
||||
|
||||
(defn build-codex-command
|
||||
[prompt {:keys [codex-bin]}]
|
||||
[(or (trim-non-empty codex-bin) "codex") "exec" "--json" prompt])
|
||||
@@ -423,7 +530,15 @@
|
||||
(def agent-bridge-registry-page "AgentBridge")
|
||||
|
||||
(def agent-bridge-registry-page-query
|
||||
'[:find [(pull ?p [:db/id :block/uuid :block/name :block/title]) ...]
|
||||
'[:find [(pull ?p [:db/id
|
||||
:block/uuid
|
||||
:block/name
|
||||
:block/title
|
||||
:logseq.property/deleted-at
|
||||
{:block/parent [:db/id
|
||||
:logseq.property/deleted-at
|
||||
{:block/parent [:db/id
|
||||
:logseq.property/deleted-at]}]}]) ...]
|
||||
:in $ ?page-name
|
||||
:where
|
||||
[?p :block/name ?page-name]])
|
||||
@@ -435,6 +550,238 @@
|
||||
[?b :block/parent ?page-id]
|
||||
[?b :block/title ?agent-name]])
|
||||
|
||||
(declare ensure-registry-page!)
|
||||
|
||||
(def agent-bridge-prompt-template-blocks-query
|
||||
'[:find [(pull ?b [:db/id
|
||||
:block/uuid
|
||||
:block/title
|
||||
:block/order
|
||||
{:block/_parent [:db/id
|
||||
:block/uuid
|
||||
:block/title
|
||||
:block/order
|
||||
{:block/_parent [:db/id
|
||||
:block/uuid
|
||||
:block/title
|
||||
:block/order]}]}]) ...]
|
||||
:in $ ?page-id
|
||||
:where
|
||||
[?b :block/parent ?page-id]])
|
||||
|
||||
(defn- prompt-template-default
|
||||
[template-kind]
|
||||
(case template-kind
|
||||
:task default-task-prompt-template
|
||||
:comment default-comment-prompt-template))
|
||||
|
||||
(defn- prompt-template-title
|
||||
[template-kind]
|
||||
(case template-kind
|
||||
:task task-prompt-template-title
|
||||
:comment comment-prompt-template-title))
|
||||
|
||||
(defn- prompt-template-var-description
|
||||
[template-kind]
|
||||
(case template-kind
|
||||
:task
|
||||
(string/join
|
||||
"\n"
|
||||
["Template variables:"
|
||||
"```text"
|
||||
"'{{graph}}': The graph name passed to `logseq agent bridge`."
|
||||
"'{{block-uuid}}': The UUID of the routed task block."
|
||||
"'{{agent-name}}': The current AgentBridge name."
|
||||
"'{{task-block-tree}}': The routed task block tree rendered as outline text."
|
||||
"```"])
|
||||
|
||||
:comment
|
||||
(string/join
|
||||
"\n"
|
||||
["Template variables:"
|
||||
"```text"
|
||||
"'{{graph}}': The graph name passed to `logseq agent bridge`."
|
||||
"'{{comment-uuid}}': The UUID of the requesting comment block."
|
||||
"'{{agent-name}}': The current AgentBridge name."
|
||||
"'{{comment-target-context}}': The block trees targeted by the comments area."
|
||||
"'{{comment-thread-context}}': The complete comments area block tree."
|
||||
"'{{requesting-comment}}': The requesting comment block tree."
|
||||
"```"])))
|
||||
|
||||
(defn- prompt-template-description
|
||||
[template-kind]
|
||||
(case template-kind
|
||||
:task
|
||||
"Description: Used when AgentBridge routes an assigned TODO Task block to Codex."
|
||||
:comment
|
||||
"Description: Used when AgentBridge routes an AgentBridge mention in a Comment block to Codex."))
|
||||
|
||||
(defn- default-prompt-template-block
|
||||
[template-kind]
|
||||
{:block/title (prompt-template-title template-kind)
|
||||
:block/children [{:block/title (prompt-template-description template-kind)}
|
||||
{:block/title (prompt-template-var-description template-kind)}
|
||||
{:block/title (str "```text\n"
|
||||
(prompt-template-default template-kind)
|
||||
"\n```")}]})
|
||||
|
||||
(defn- ensure-prompt-template-block-uuids
|
||||
[blocks]
|
||||
(mapv (fn ensure-block-uuid [block]
|
||||
(let [block (cond-> block
|
||||
(nil? (:block/uuid block))
|
||||
(assoc :block/uuid (random-uuid)))]
|
||||
(if (seq (:block/children block))
|
||||
(update block :block/children ensure-prompt-template-block-uuids)
|
||||
block)))
|
||||
blocks))
|
||||
|
||||
(defn- flatten-prompt-template-blocks
|
||||
[blocks]
|
||||
(letfn [(walk [parent-uuid block]
|
||||
(let [children (:block/children block)
|
||||
block-uuid (:block/uuid block)
|
||||
block (cond-> (dissoc block :block/children)
|
||||
parent-uuid
|
||||
(assoc :block/parent [:block/uuid parent-uuid]))]
|
||||
(into [block]
|
||||
(mapcat #(walk block-uuid %) children))))]
|
||||
(->> blocks
|
||||
ensure-prompt-template-block-uuids
|
||||
(mapcat #(walk nil %))
|
||||
vec)))
|
||||
|
||||
(defn- child-blocks
|
||||
[block]
|
||||
(->> (or (:block/children block)
|
||||
(:block/_parent block)
|
||||
[])
|
||||
(sort-by #(or (:block/order %) 0))))
|
||||
|
||||
(defn- block-title-tree
|
||||
[block]
|
||||
(cons (:block/title block)
|
||||
(mapcat block-title-tree (child-blocks block))))
|
||||
|
||||
(defn- code-blocks-in-text
|
||||
[text]
|
||||
(when (string? text)
|
||||
(map second (re-seq #"(?s)```[^\n`]*\n(.*?)```" text))))
|
||||
|
||||
(defn- prompt-template-from-block
|
||||
[template-kind block]
|
||||
(let [templates (vec (mapcat code-blocks-in-text (block-title-tree block)))
|
||||
renderable-templates (filterv #(-> (validate-prompt-template template-kind %) :ok?)
|
||||
templates)]
|
||||
(cond
|
||||
(empty? templates)
|
||||
(throw (ex-info "agent bridge prompt template code block is missing"
|
||||
{:code :agent-prompt-template-invalid
|
||||
:reason :missing-template-code-block
|
||||
:template template-kind}))
|
||||
|
||||
(= 1 (count renderable-templates))
|
||||
(first renderable-templates)
|
||||
|
||||
(> (count renderable-templates) 1)
|
||||
(throw (ex-info "agent bridge prompt template must contain one code block"
|
||||
{:code :agent-prompt-template-invalid
|
||||
:reason :multiple-template-code-blocks
|
||||
:template template-kind}))
|
||||
|
||||
(= 1 (count templates))
|
||||
(do
|
||||
(validate-prompt-templates! {template-kind (first templates)})
|
||||
(first templates))
|
||||
|
||||
:else
|
||||
(throw (ex-info "agent bridge prompt template code block is missing"
|
||||
{:code :agent-prompt-template-invalid
|
||||
:reason :missing-template-code-block
|
||||
:template template-kind})))))
|
||||
|
||||
(defn- missing-template-code-block-error?
|
||||
[error]
|
||||
(let [data (ex-data error)]
|
||||
(and (= :agent-prompt-template-invalid (:code data))
|
||||
(= :missing-template-code-block (:reason data)))))
|
||||
|
||||
(defn- prompt-template-blocks-by-title
|
||||
[blocks]
|
||||
(reduce (fn [acc block]
|
||||
(let [title (:block/title block)]
|
||||
(if (contains? #{task-prompt-template-title
|
||||
comment-prompt-template-title}
|
||||
title)
|
||||
(assoc acc title block)
|
||||
acc)))
|
||||
{}
|
||||
blocks))
|
||||
|
||||
(defn ensure-agent-bridge-prompt-templates!
|
||||
[cfg repo]
|
||||
(p/let [page (ensure-registry-page! cfg repo)
|
||||
page-id (:db/id page)
|
||||
page-uuid (:block/uuid page)
|
||||
_ (when-not page-id
|
||||
(throw (ex-info "agent bridge registry page not found"
|
||||
{:code :agent-prompt-template-initialization-failed})))
|
||||
_ (when-not page-uuid
|
||||
(throw (ex-info "agent bridge registry page uuid not found"
|
||||
{:code :agent-prompt-template-initialization-failed})))
|
||||
blocks (transport/invoke cfg :thread-api/q
|
||||
[repo [agent-bridge-prompt-template-blocks-query page-id]])]
|
||||
(let [blocks-by-title (prompt-template-blocks-by-title blocks)
|
||||
template-state (reduce (fn [state template-kind]
|
||||
(if-let [block (get blocks-by-title (prompt-template-title template-kind))]
|
||||
(try
|
||||
(assoc-in state [:templates template-kind]
|
||||
(prompt-template-from-block template-kind block))
|
||||
(catch :default e
|
||||
(if (missing-template-code-block-error? e)
|
||||
(assoc-in state [:repair-blocks template-kind] block)
|
||||
(throw e))))
|
||||
(update state :missing-kinds conj template-kind)))
|
||||
{:templates {}
|
||||
:missing-kinds []
|
||||
:repair-blocks {}}
|
||||
[:task :comment])
|
||||
existing-templates (:templates template-state)
|
||||
_ (validate-prompt-templates! existing-templates)
|
||||
missing-kinds (:missing-kinds template-state)
|
||||
repair-blocks (:repair-blocks template-state)]
|
||||
(p/let [_ (when (seq missing-kinds)
|
||||
(transport/invoke cfg :thread-api/apply-outliner-ops
|
||||
[repo [[:insert-blocks [(flatten-prompt-template-blocks
|
||||
(mapv default-prompt-template-block missing-kinds))
|
||||
page-uuid
|
||||
{:outliner-op :insert-blocks
|
||||
:sibling? false
|
||||
:bottom? true
|
||||
:keep-uuid? true}]]]
|
||||
{}]))
|
||||
_ (when (seq repair-blocks)
|
||||
(p/all
|
||||
(mapv (fn [[template-kind block]]
|
||||
(transport/invoke cfg :thread-api/apply-outliner-ops
|
||||
[repo [[:insert-blocks [(flatten-prompt-template-blocks
|
||||
(:block/children (default-prompt-template-block template-kind)))
|
||||
(:block/uuid block)
|
||||
{:outliner-op :insert-blocks
|
||||
:sibling? false
|
||||
:bottom? true
|
||||
:keep-uuid? true}]]]
|
||||
{}]))
|
||||
repair-blocks)))]
|
||||
(validate-prompt-templates!
|
||||
(reduce (fn [templates template-kind]
|
||||
(assoc templates
|
||||
template-kind
|
||||
(or (get existing-templates template-kind)
|
||||
(prompt-template-default template-kind))))
|
||||
{}
|
||||
[:task :comment]))))))
|
||||
|
||||
(defn random-bridge-block-uuid
|
||||
[]
|
||||
(random-uuid))
|
||||
@@ -443,6 +790,10 @@
|
||||
[entities]
|
||||
(first (filter :db/id entities)))
|
||||
|
||||
(defn- first-live-entity
|
||||
[entities]
|
||||
(first (remove ldb/recycled? (filter :db/id entities))))
|
||||
|
||||
(defn- registry-page-name
|
||||
[]
|
||||
(common-util/page-name-sanity-lc agent-bridge-registry-page))
|
||||
@@ -452,7 +803,7 @@
|
||||
(p/let [pages (transport/invoke cfg :thread-api/q
|
||||
[repo [agent-bridge-registry-page-query
|
||||
(registry-page-name)]])]
|
||||
(first-entity pages)))
|
||||
(first-live-entity pages)))
|
||||
|
||||
(defn- ensure-registry-page!
|
||||
[cfg repo]
|
||||
@@ -576,12 +927,13 @@
|
||||
(map #(assoc % "Assignee" agent-name) blocks))))))
|
||||
|
||||
(defn- dry-run-commands
|
||||
[graph agent-name tasks]
|
||||
[graph agent-name prompt-templates tasks]
|
||||
(mapv (fn [{:keys [block tree-text]}]
|
||||
(let [prompt (build-codex-prompt {:graph graph
|
||||
:agent-name agent-name
|
||||
:block block
|
||||
:tree-text tree-text})
|
||||
:tree-text tree-text
|
||||
:prompt-template (:task prompt-templates)})
|
||||
command (build-codex-command prompt {})]
|
||||
{:block (block-uuid-str block)
|
||||
:backend :codex
|
||||
@@ -607,11 +959,12 @@
|
||||
:updated-at (js/Date.now)})
|
||||
|
||||
(defn- route-task!
|
||||
[cfg {:keys [repo graph agent-name]} {:keys [block tree-text]}]
|
||||
[cfg {:keys [repo graph agent-name prompt-templates]} {:keys [block tree-text]}]
|
||||
(let [prompt (build-codex-prompt {:graph graph
|
||||
:agent-name agent-name
|
||||
:block block
|
||||
:tree-text tree-text})
|
||||
:tree-text tree-text
|
||||
:prompt-template (:task prompt-templates)})
|
||||
command (build-codex-command prompt {})
|
||||
preview (command-preview command)]
|
||||
(emit-log! cfg (log-line (str "Codex command prepared for " (block-uuid-str block) ": " preview)))
|
||||
@@ -660,11 +1013,12 @@
|
||||
(route-task! cfg opts task))))
|
||||
|
||||
(defn- process-tasks!
|
||||
[cfg {:keys [repo graph agent-name]}]
|
||||
[cfg {:keys [repo graph agent-name prompt-templates]}]
|
||||
(p/let [tasks (list-routable-tasks cfg repo agent-name)]
|
||||
(p/all (mapv #(route-task-once! cfg {:repo repo
|
||||
:graph graph
|
||||
:agent-name agent-name}
|
||||
:agent-name agent-name
|
||||
:prompt-templates prompt-templates}
|
||||
%)
|
||||
tasks))))
|
||||
|
||||
@@ -897,7 +1251,7 @@
|
||||
trim-non-empty))))
|
||||
|
||||
(defn- route-comment!
|
||||
[cfg {:keys [repo graph agent-name]} comment-block]
|
||||
[cfg {:keys [repo graph agent-name prompt-templates]} comment-block]
|
||||
(p/catch
|
||||
(p/let [comment-uuid (:block/uuid comment-block)
|
||||
_ (when-not comment-uuid
|
||||
@@ -920,7 +1274,8 @@
|
||||
:comment comment-block
|
||||
:target-tree-texts target-tree-texts
|
||||
:comments-area-tree-text comments-area-tree-text
|
||||
:comment-tree-text comment-tree-text})
|
||||
:comment-tree-text comment-tree-text
|
||||
:prompt-template (:comment prompt-templates)})
|
||||
resume-session-id (some #(comment-target-session-id agent-name session-property-ident %) target-blocks)
|
||||
command (if resume-session-id
|
||||
(build-codex-resume-command resume-session-id prompt {})
|
||||
@@ -997,7 +1352,7 @@
|
||||
(p/all routing)))))
|
||||
|
||||
(defn- listen-forever!
|
||||
[cfg {:keys [repo graph agent-name]}]
|
||||
[cfg {:keys [repo graph agent-name prompt-templates]}]
|
||||
(let [routing-blocks* (atom #{})
|
||||
handle-error! (fn [e]
|
||||
(emit-log! cfg (log-line (str "Codex invocation failed: "
|
||||
@@ -1015,6 +1370,7 @@
|
||||
{:repo repo
|
||||
:graph graph
|
||||
:agent-name agent-name
|
||||
:prompt-templates prompt-templates
|
||||
:routing-blocks* routing-blocks*}
|
||||
payload)
|
||||
(p/catch handle-error!))
|
||||
@@ -1044,11 +1400,13 @@
|
||||
(if-not (codex-available? nil)
|
||||
(bridge-error :codex-not-found "codex executable is not available")
|
||||
(p/let [cfg (cli-server/ensure-server! config repo)
|
||||
logs (conj logs (log-line "checking prompt templates ..."))
|
||||
prompt-templates (ensure-agent-bridge-prompt-templates! cfg repo)
|
||||
logs (conj logs (log-line "registering agent bridge ..."))
|
||||
_ (register-agent-bridge! cfg repo agent-name)]
|
||||
(if (:dry-run? action)
|
||||
(p/let [tasks (list-routable-tasks cfg repo agent-name)
|
||||
commands (dry-run-commands graph agent-name tasks)
|
||||
commands (dry-run-commands graph agent-name prompt-templates tasks)
|
||||
logs (into (conj logs (log-line "listening graph changes ..."))
|
||||
(map (fn [{:keys [block preview]}]
|
||||
(log-line (str "would run Codex command for " block ": " preview)))
|
||||
@@ -1066,7 +1424,8 @@
|
||||
(if (:process-once? action)
|
||||
(p/let [routed (process-tasks! cfg {:repo repo
|
||||
:graph graph
|
||||
:agent-name agent-name})]
|
||||
:agent-name agent-name
|
||||
:prompt-templates prompt-templates})]
|
||||
{:status :ok
|
||||
:command :agent-bridge
|
||||
:data {:mode :processed-once
|
||||
@@ -1075,7 +1434,9 @@
|
||||
:routed routed}})
|
||||
(p/let [_ (process-tasks! cfg {:repo repo
|
||||
:graph graph
|
||||
:agent-name agent-name})]
|
||||
:agent-name agent-name
|
||||
:prompt-templates prompt-templates})]
|
||||
(listen-forever! cfg {:repo repo
|
||||
:graph graph
|
||||
:agent-name agent-name}))))))))))))
|
||||
:agent-name agent-name
|
||||
:prompt-templates prompt-templates}))))))))))))
|
||||
|
||||
@@ -211,6 +211,268 @@
|
||||
(is (string/starts-with? preview "codex exec --json '"))
|
||||
(is (string/includes? preview "Ship the CLI bridge")))))
|
||||
|
||||
(deftest test-prompt-templates
|
||||
(testing "prompt builders render supplied graph templates"
|
||||
(let [prompt (agent-command/build-codex-prompt
|
||||
{:graph "demo"
|
||||
:agent-name "build-host"
|
||||
:block (task-block {})
|
||||
:tree-text "- Ship the CLI bridge"
|
||||
:prompt-template "Task {{graph}} {{block-uuid}} {{agent-name}}\n{{task-block-tree}}"})
|
||||
comment-prompt (#'agent-command/build-comment-codex-prompt
|
||||
{:graph "demo"
|
||||
:agent-name "build-host"
|
||||
:comment (comment-block {})
|
||||
:target-tree-texts ["- Target block"]
|
||||
:comments-area-tree-text "- Comments"
|
||||
:comment-tree-text "- [[build-host]] summarize"
|
||||
:prompt-template "Comment {{graph}} {{comment-uuid}} {{agent-name}}\n{{comment-target-context}}\n{{comment-thread-context}}\n{{requesting-comment}}"})]
|
||||
(is (= "Task demo 11111111-1111-1111-1111-111111111111 build-host\n- Ship the CLI bridge"
|
||||
prompt))
|
||||
(is (string/includes? comment-prompt
|
||||
"Comment demo 55555555-5555-5555-5555-555555555555 build-host"))
|
||||
(is (string/includes? comment-prompt "- Target block"))
|
||||
(is (string/includes? comment-prompt "- [[build-host]] summarize"))))
|
||||
|
||||
(testing "template lint fails on missing and unknown vars"
|
||||
(let [missing-result (#'agent-command/validate-prompt-template
|
||||
:task
|
||||
"Task {{graph}} {{block-uuid}} {{agent-name}}")
|
||||
unknown-result (#'agent-command/validate-prompt-template
|
||||
:task
|
||||
"Task {{graph}} {{block-uuid}} {{agent-name}} {{task-block-tree}} {{extra}}")]
|
||||
(is (false? (:ok? missing-result)))
|
||||
(is (= :missing-template-vars (get-in missing-result [:error :code])))
|
||||
(is (= #{"task-block-tree"} (get-in missing-result [:error :vars])))
|
||||
(is (false? (:ok? unknown-result)))
|
||||
(is (= :unknown-template-vars (get-in unknown-result [:error :code])))
|
||||
(is (= #{"extra"} (get-in unknown-result [:error :vars]))))))
|
||||
|
||||
(deftest test-prompt-template-reader-ignores-documentation-code-fences
|
||||
(testing "template reader ignores variable documentation code fences"
|
||||
(let [template (#'agent-command/prompt-template-from-block
|
||||
:task
|
||||
{:block/title agent-command/task-prompt-template-title
|
||||
:block/children [{:block/title "```text\n'{{graph}}': Graph name\n'{{block-uuid}}': Block UUID\n'{{agent-name}}': AgentBridge name\n'{{task-block-tree}}': Task tree\n```"}
|
||||
{:block/title "```text\nTask {{graph}} {{block-uuid}} {{agent-name}}\n{{task-block-tree}}\n```"}]})]
|
||||
(is (= "Task {{graph}} {{block-uuid}} {{agent-name}}\n{{task-block-tree}}\n"
|
||||
template)))))
|
||||
|
||||
(deftest test-agent-bridge-initializes-default-prompt-templates
|
||||
(async done
|
||||
(let [calls* (atom [])
|
||||
page-uuid (uuid "33333333-3333-3333-3333-333333333333")]
|
||||
(-> (p/with-redefs [transport/invoke
|
||||
(fn [_cfg method args]
|
||||
(swap! calls* conj [method args])
|
||||
(case method
|
||||
:thread-api/q
|
||||
(let [[_ [query & _query-args]] args]
|
||||
(cond
|
||||
(= query agent-command/agent-bridge-registry-page-query)
|
||||
(p/resolved [])
|
||||
|
||||
(= query agent-command/agent-bridge-prompt-template-blocks-query)
|
||||
(p/resolved [])
|
||||
|
||||
:else
|
||||
(p/rejected (ex-info "unexpected query"
|
||||
{:query query}))))
|
||||
|
||||
:thread-api/apply-outliner-ops
|
||||
(let [[_ ops _] args]
|
||||
(if (= [[:create-page [agent-command/agent-bridge-registry-page {}]]] ops)
|
||||
(p/resolved [agent-command/agent-bridge-registry-page page-uuid])
|
||||
(p/resolved {:ok true})))
|
||||
|
||||
:thread-api/pull
|
||||
(p/resolved {:db/id 300
|
||||
:block/uuid page-uuid
|
||||
:block/title agent-command/agent-bridge-registry-page})
|
||||
|
||||
(p/rejected (ex-info "unexpected invoke"
|
||||
{:method method
|
||||
:args args}))))]
|
||||
(p/let [templates (agent-command/ensure-agent-bridge-prompt-templates!
|
||||
{:root-dir "/tmp/logseq"}
|
||||
"logseq_db_demo")]
|
||||
(is (string/includes? (:task templates) "{{task-block-tree}}"))
|
||||
(is (string/includes? (:comment templates) "{{requesting-comment}}"))
|
||||
(let [insert-ops (->> @calls*
|
||||
(filter #(= :thread-api/apply-outliner-ops (first %)))
|
||||
(mapv (comp second second)))]
|
||||
(is (= [[:create-page [agent-command/agent-bridge-registry-page {}]]]
|
||||
(first insert-ops)))
|
||||
(let [inserted-blocks (-> (second insert-ops) first second first)
|
||||
root-blocks (filter #(nil? (:block/parent %)) inserted-blocks)
|
||||
root-uuids (set (map :block/uuid root-blocks))
|
||||
child-blocks (remove #(nil? (:block/parent %)) inserted-blocks)]
|
||||
(is (= #{agent-command/task-prompt-template-title
|
||||
agent-command/comment-prompt-template-title}
|
||||
(set (map :block/title root-blocks))))
|
||||
(is (= 8 (count inserted-blocks)))
|
||||
(is (some #(string/includes? (:block/title %) "```text\n'{{graph}}'")
|
||||
child-blocks))
|
||||
(is (not-any? #(re-find #"- \{\{[A-Za-z0-9-]+\}\}:"
|
||||
(:block/title %))
|
||||
child-blocks))
|
||||
(is (every? #(contains? root-uuids (second (:block/parent %))) child-blocks))))))
|
||||
(p/catch (fn [e]
|
||||
(is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest test-agent-bridge-ignores-recycled-registry-page
|
||||
(async done
|
||||
(let [calls* (atom [])
|
||||
recycled-page {:db/id 188
|
||||
:block/uuid (uuid "22222222-2222-2222-2222-222222222222")
|
||||
:block/name "agentbridge"
|
||||
:block/title agent-command/agent-bridge-registry-page
|
||||
:block/parent {:db/id 183
|
||||
:logseq.property/deleted-at 1}}
|
||||
live-page-uuid (uuid "33333333-3333-3333-3333-333333333333")
|
||||
live-page {:db/id 300
|
||||
:block/uuid live-page-uuid
|
||||
:block/name "agentbridge"
|
||||
:block/title agent-command/agent-bridge-registry-page}]
|
||||
(-> (p/with-redefs [transport/invoke
|
||||
(fn [_cfg method args]
|
||||
(swap! calls* conj [method args])
|
||||
(case method
|
||||
:thread-api/q
|
||||
(let [[_ [query & _query-args]] args]
|
||||
(cond
|
||||
(= query agent-command/agent-bridge-registry-page-query)
|
||||
(p/resolved [recycled-page])
|
||||
|
||||
(= query agent-command/agent-bridge-prompt-template-blocks-query)
|
||||
(p/resolved [])
|
||||
|
||||
:else
|
||||
(p/rejected (ex-info "unexpected query"
|
||||
{:query query}))))
|
||||
|
||||
:thread-api/apply-outliner-ops
|
||||
(let [[_ ops _] args]
|
||||
(if (= [[:create-page [agent-command/agent-bridge-registry-page {}]]] ops)
|
||||
(p/resolved [agent-command/agent-bridge-registry-page live-page-uuid])
|
||||
(p/resolved {:ok true})))
|
||||
|
||||
:thread-api/pull
|
||||
(p/resolved live-page)
|
||||
|
||||
(p/rejected (ex-info "unexpected invoke"
|
||||
{:method method
|
||||
:args args}))))]
|
||||
(p/let [templates (agent-command/ensure-agent-bridge-prompt-templates!
|
||||
{:root-dir "/tmp/logseq"}
|
||||
"logseq_db_demo")]
|
||||
(is (string/includes? (:task templates) "{{task-block-tree}}"))
|
||||
(is (some #(= [:thread-api/apply-outliner-ops
|
||||
["logseq_db_demo"
|
||||
[[:create-page [agent-command/agent-bridge-registry-page {}]]]
|
||||
{}]]
|
||||
%)
|
||||
@calls*))))
|
||||
(p/catch (fn [e]
|
||||
(is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest test-agent-bridge-repairs-template-root-without-code-block
|
||||
(async done
|
||||
(let [calls* (atom [])
|
||||
page-uuid (uuid "33333333-3333-3333-3333-333333333333")
|
||||
comment-template-uuid (uuid "44444444-4444-4444-4444-444444444444")
|
||||
page {:db/id 300
|
||||
:block/uuid page-uuid
|
||||
:block/title agent-command/agent-bridge-registry-page}
|
||||
broken-comment-template {:db/id 401
|
||||
:block/uuid comment-template-uuid
|
||||
:block/title agent-command/comment-prompt-template-title}]
|
||||
(-> (p/with-redefs [transport/invoke
|
||||
(fn [_cfg method args]
|
||||
(swap! calls* conj [method args])
|
||||
(case method
|
||||
:thread-api/q
|
||||
(let [[_ [query & _query-args]] args]
|
||||
(cond
|
||||
(= query agent-command/agent-bridge-registry-page-query)
|
||||
(p/resolved [page])
|
||||
|
||||
(= query agent-command/agent-bridge-prompt-template-blocks-query)
|
||||
(p/resolved [broken-comment-template])
|
||||
|
||||
:else
|
||||
(p/rejected (ex-info "unexpected query"
|
||||
{:query query}))))
|
||||
|
||||
:thread-api/apply-outliner-ops
|
||||
(p/resolved {:ok true})
|
||||
|
||||
(p/rejected (ex-info "unexpected invoke"
|
||||
{:method method
|
||||
:args args}))))]
|
||||
(p/let [templates (agent-command/ensure-agent-bridge-prompt-templates!
|
||||
{:root-dir "/tmp/logseq"}
|
||||
"logseq_db_demo")]
|
||||
(is (string/includes? (:task templates) "{{task-block-tree}}"))
|
||||
(is (string/includes? (:comment templates) "{{requesting-comment}}"))
|
||||
(let [insert-ops (->> @calls*
|
||||
(filter #(= :thread-api/apply-outliner-ops (first %)))
|
||||
(mapv (comp second second)))
|
||||
child-repair-op (first (filter #(= comment-template-uuid
|
||||
(-> % first second second))
|
||||
insert-ops))]
|
||||
(is (some? child-repair-op))
|
||||
(is (some #(string/includes? (:block/title %) "{{requesting-comment}}")
|
||||
(-> child-repair-op first second first))))))
|
||||
(p/catch (fn [e]
|
||||
(is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest test-agent-bridge-template-initialization-lints-existing-page
|
||||
(async done
|
||||
(let [bad-template-block {:db/id 401
|
||||
:block/title agent-command/task-prompt-template-title
|
||||
:block/children [{:db/id 402
|
||||
:block/title "```text\nTask {{graph}} {{block-uuid}} {{agent-name}} {{unknown}}\n```"}]}
|
||||
page {:db/id 300
|
||||
:block/uuid (uuid "33333333-3333-3333-3333-333333333333")
|
||||
:block/title agent-command/agent-bridge-registry-page}]
|
||||
(-> (p/with-redefs [transport/invoke
|
||||
(fn [_cfg method args]
|
||||
(case method
|
||||
:thread-api/q
|
||||
(let [[_ [query & _query-args]] args]
|
||||
(cond
|
||||
(= query agent-command/agent-bridge-registry-page-query)
|
||||
(p/resolved [page])
|
||||
|
||||
(= query agent-command/agent-bridge-prompt-template-blocks-query)
|
||||
(p/resolved [bad-template-block])
|
||||
|
||||
:else
|
||||
(p/rejected (ex-info "unexpected query"
|
||||
{:query query}))))
|
||||
|
||||
:thread-api/apply-outliner-ops
|
||||
(p/resolved {:ok true})
|
||||
|
||||
(p/rejected (ex-info "unexpected invoke"
|
||||
{:method method
|
||||
:args args}))))]
|
||||
(agent-command/ensure-agent-bridge-prompt-templates!
|
||||
{:root-dir "/tmp/logseq"}
|
||||
"logseq_db_demo"))
|
||||
(p/then (fn [_]
|
||||
(is false "expected template lint to fail")))
|
||||
(p/catch (fn [e]
|
||||
(is (= :agent-prompt-template-invalid
|
||||
(:code (ex-data e))))
|
||||
(is (= :task
|
||||
(:template (ex-data e))))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest test-agent-bridge-listener-routes-comment-mention-with-context-and-reactions
|
||||
(async done
|
||||
(let [root (temp-root)
|
||||
@@ -886,6 +1148,11 @@
|
||||
agent-command/register-agent-bridge! (fn [cfg repo agent-name]
|
||||
(swap! calls conj [:register (:root-dir cfg) repo agent-name])
|
||||
(p/resolved true))
|
||||
agent-command/ensure-agent-bridge-prompt-templates!
|
||||
(fn [cfg repo]
|
||||
(swap! calls conj [:prompt-templates (:root-dir cfg) repo])
|
||||
(p/resolved {:task "Custom task {{graph}} {{block-uuid}} {{agent-name}}\n{{task-block-tree}}"
|
||||
:comment "Custom comment {{graph}} {{comment-uuid}} {{agent-name}}\n{{comment-target-context}}\n{{comment-thread-context}}\n{{requesting-comment}}"}))
|
||||
agent-command/list-routable-tasks (fn [_cfg repo agent-name]
|
||||
(swap! calls conj [:list repo agent-name])
|
||||
(p/resolved [{:block (task-block {})
|
||||
@@ -899,10 +1166,15 @@
|
||||
(is (= :ok (:status result)))
|
||||
(is (= :dry-run (get-in result [:data :mode])))
|
||||
(is (= [[:ensure-server "/tmp/logseq" "logseq_db_demo"]
|
||||
[:prompt-templates "/tmp/logseq" "logseq_db_demo"]
|
||||
[:register "/tmp/logseq" "logseq_db_demo" "build-host"]
|
||||
[:list "logseq_db_demo" "build-host"]]
|
||||
@calls))
|
||||
(is (= 1 (count (get-in result [:data :commands]))))
|
||||
(is (string/includes? (get-in result [:data :commands 0 :command 3])
|
||||
"Custom task demo"))
|
||||
(is (not (string/includes? (get-in result [:data :commands 0 :command 3])
|
||||
"You are handling a Logseq AgentBridge task.")))
|
||||
(is (string/includes? (first (get-in result [:data :logs]))
|
||||
"checking the environment"))
|
||||
(is (string/includes? (last (get-in result [:data :logs]))
|
||||
@@ -926,6 +1198,11 @@
|
||||
agent-command/register-agent-bridge! (fn [_cfg repo agent-name]
|
||||
(swap! calls conj [:register repo agent-name])
|
||||
(p/resolved true))
|
||||
agent-command/ensure-agent-bridge-prompt-templates!
|
||||
(fn [_cfg repo]
|
||||
(swap! calls conj [:prompt-templates repo])
|
||||
(p/resolved {:task "Task {{graph}} {{block-uuid}} {{agent-name}}\n{{task-block-tree}}"
|
||||
:comment "Comment {{graph}} {{comment-uuid}} {{agent-name}}\n{{comment-target-context}}\n{{comment-thread-context}}\n{{requesting-comment}}"}))
|
||||
agent-command/list-routable-tasks (fn [_cfg repo agent-name]
|
||||
(swap! calls conj [:list repo agent-name])
|
||||
(p/resolved [{:block block
|
||||
@@ -948,13 +1225,10 @@
|
||||
(is (= :ok (:status result)))
|
||||
(is (= :processed-once (get-in result [:data :mode])))
|
||||
(is (= [[:ensure-server root "logseq_db_demo"]
|
||||
[:prompt-templates "logseq_db_demo"]
|
||||
[:register "logseq_db_demo" "build-host"]
|
||||
[:list "logseq_db_demo" "build-host"]
|
||||
[:codex ["codex" "exec" "--json" (agent-command/build-codex-prompt
|
||||
{:graph "demo"
|
||||
:agent-name "build-host"
|
||||
:block block
|
||||
:tree-text "- Ship the CLI bridge"})]]
|
||||
[:codex ["codex" "exec" "--json" "Task demo 11111111-1111-1111-1111-111111111111 build-host\n- Ship the CLI bridge"]]
|
||||
[:ensure-server root "logseq_db_demo"]
|
||||
[:write-session "logseq_db_demo" (:block/uuid block) "session-123"]]
|
||||
@calls))
|
||||
|
||||
Reference in New Issue
Block a user