Spaces:
Running on Zero
Running on Zero
feat: export field notes
Browse filesCo-authored-by: Codex <noreply@openai.com>
- README.md +6 -0
- app.py +16 -0
- hackathon_advisor/agent.py +2 -0
- hackathon_advisor/field_notes.py +137 -0
- static/app.js +23 -3
- static/index.html +1 -0
- static/styles.css +1 -1
- tests/test_app.py +27 -1
- tests/test_field_notes.py +34 -0
README.md
CHANGED
|
@@ -63,6 +63,12 @@ source, project order, and digest before the app starts.
|
|
| 63 |
The app exposes a `trace_artifact` Gradio API endpoint and a `JSONL` button in the UI. Both emit the same JSONL schema:
|
| 64 |
a manifest row followed by one row per agent turn. `data/sample_trace.jsonl` is a checked-in, Hub-published sample trace.
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
## Tool-Call Contract
|
| 67 |
|
| 68 |
`/api/tool-contracts` exposes the JSON schemas intended for MiniCPM-style tool calling. `tool_contract_check` accepts a
|
|
|
|
| 63 |
The app exposes a `trace_artifact` Gradio API endpoint and a `JSONL` button in the UI. Both emit the same JSONL schema:
|
| 64 |
a manifest row followed by one row per agent turn. `data/sample_trace.jsonl` is a checked-in, Hub-published sample trace.
|
| 65 |
|
| 66 |
+
## Field Notes Artifact
|
| 67 |
+
|
| 68 |
+
The `field_notes` Gradio API endpoint and `Notes` button export a Markdown build note from the exact session state:
|
| 69 |
+
builder profile, target badges, idea board, cited Spaces, latest build plan, planner calls, and the share caption. This
|
| 70 |
+
keeps the Field Notes badge path tied to auditable app evidence instead of a separate hand-written summary.
|
| 71 |
+
|
| 72 |
## Tool-Call Contract
|
| 73 |
|
| 74 |
`/api/tool-contracts` exposes the JSON schemas intended for MiniCPM-style tool calling. `tool_contract_check` accepts a
|
app.py
CHANGED
|
@@ -10,6 +10,7 @@ from gradio import Server
|
|
| 10 |
|
| 11 |
from hackathon_advisor.agent import AdvisorEngine
|
| 12 |
from hackathon_advisor.data import ProjectIndex
|
|
|
|
| 13 |
from hackathon_advisor.tool_contracts import resolve_tool_call, tool_schemas
|
| 14 |
from hackathon_advisor.tools import TARGETS
|
| 15 |
from hackathon_advisor.trace_export import build_trace_jsonl, trace_metadata
|
|
@@ -94,6 +95,21 @@ def trace_artifact(session_json: str = "{}") -> str:
|
|
| 94 |
return build_trace_jsonl(session, trace_metadata(index))
|
| 95 |
|
| 96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
@app.api(name="agent_turn", concurrency_limit=4, stream_every=0.04)
|
| 98 |
def agent_turn(message: str, session_json: str = "{}") -> Iterator[str]:
|
| 99 |
try:
|
|
|
|
| 10 |
|
| 11 |
from hackathon_advisor.agent import AdvisorEngine
|
| 12 |
from hackathon_advisor.data import ProjectIndex
|
| 13 |
+
from hackathon_advisor.field_notes import build_field_notes_markdown
|
| 14 |
from hackathon_advisor.tool_contracts import resolve_tool_call, tool_schemas
|
| 15 |
from hackathon_advisor.tools import TARGETS
|
| 16 |
from hackathon_advisor.trace_export import build_trace_jsonl, trace_metadata
|
|
|
|
| 95 |
return build_trace_jsonl(session, trace_metadata(index))
|
| 96 |
|
| 97 |
|
| 98 |
+
@app.api(name="field_notes", concurrency_limit=8)
|
| 99 |
+
def field_notes_artifact(session_json: str = "{}") -> str:
|
| 100 |
+
try:
|
| 101 |
+
session = json.loads(session_json or "{}")
|
| 102 |
+
except json.JSONDecodeError:
|
| 103 |
+
session = {}
|
| 104 |
+
return build_field_notes_markdown(
|
| 105 |
+
session,
|
| 106 |
+
{
|
| 107 |
+
**trace_metadata(index),
|
| 108 |
+
"project_count": len(index.projects),
|
| 109 |
+
},
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
@app.api(name="agent_turn", concurrency_limit=4, stream_every=0.04)
|
| 114 |
def agent_turn(message: str, session_json: str = "{}") -> Iterator[str]:
|
| 115 |
try:
|
hackathon_advisor/agent.py
CHANGED
|
@@ -353,6 +353,8 @@ class AdvisorEngine:
|
|
| 353 |
}
|
| 354 |
)
|
| 355 |
state["trace"] = trace[-12:]
|
|
|
|
|
|
|
| 356 |
if artifact:
|
| 357 |
state["last_artifact"] = artifact
|
| 358 |
|
|
|
|
| 353 |
}
|
| 354 |
)
|
| 355 |
state["trace"] = trace[-12:]
|
| 356 |
+
if plan:
|
| 357 |
+
state["last_plan"] = list(plan)
|
| 358 |
if artifact:
|
| 359 |
state["last_artifact"] = artifact
|
| 360 |
|
hackathon_advisor/field_notes.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from datetime import datetime, timezone
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def build_field_notes_markdown(session: dict[str, Any], metadata: dict[str, Any]) -> str:
|
| 8 |
+
ideas = _list_of_dicts(session.get("ideas"))
|
| 9 |
+
trace = _list_of_dicts(session.get("trace"))
|
| 10 |
+
profile = session.get("profile") if isinstance(session.get("profile"), dict) else {}
|
| 11 |
+
targets = [str(target) for target in session.get("targets") or []]
|
| 12 |
+
last_plan = [str(step) for step in session.get("last_plan") or []]
|
| 13 |
+
last_artifact = session.get("last_artifact") if isinstance(session.get("last_artifact"), dict) else {}
|
| 14 |
+
|
| 15 |
+
lines = [
|
| 16 |
+
"# Hackathon Advisor Field Notes",
|
| 17 |
+
"",
|
| 18 |
+
f"Generated: {datetime.now(timezone.utc).isoformat(timespec='seconds')}",
|
| 19 |
+
"",
|
| 20 |
+
"## Snapshot",
|
| 21 |
+
"",
|
| 22 |
+
f"- Project snapshot: {_clean(metadata.get('snapshot_generated_at'))}",
|
| 23 |
+
f"- Project count: {_clean(metadata.get('project_count', 'unknown'))}",
|
| 24 |
+
f"- Index: {_clean(metadata.get('index_algorithm'))} at {_clean(metadata.get('index_generated_at'))}",
|
| 25 |
+
f"- Digest: `{_clean(metadata.get('snapshot_digest'))}`",
|
| 26 |
+
"",
|
| 27 |
+
"## Builder Context",
|
| 28 |
+
"",
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
if profile:
|
| 32 |
+
for field in ("skills", "time", "preferences", "constraints"):
|
| 33 |
+
value = _clean(profile.get(field))
|
| 34 |
+
if value:
|
| 35 |
+
lines.append(f"- {field.title()}: {value}")
|
| 36 |
+
else:
|
| 37 |
+
lines.append("- No profile notes recorded.")
|
| 38 |
+
lines.append(f"- Targets: {', '.join(targets) if targets else 'No specific targets'}")
|
| 39 |
+
|
| 40 |
+
lines.extend(["", "## Idea Board", ""])
|
| 41 |
+
if ideas:
|
| 42 |
+
for index, idea in enumerate(ideas, start=1):
|
| 43 |
+
lines.extend(_idea_section(index, idea))
|
| 44 |
+
else:
|
| 45 |
+
lines.append("No ideas were written.")
|
| 46 |
+
|
| 47 |
+
lines.extend(["", "## Build Plan", ""])
|
| 48 |
+
if last_plan:
|
| 49 |
+
for index, step in enumerate(last_plan, start=1):
|
| 50 |
+
lines.append(f"{index}. {_clean(step)}")
|
| 51 |
+
else:
|
| 52 |
+
lines.append("No build plan was pressed yet.")
|
| 53 |
+
|
| 54 |
+
lines.extend(["", "## Turn Trace", ""])
|
| 55 |
+
if trace:
|
| 56 |
+
for index, event in enumerate(trace, start=1):
|
| 57 |
+
lines.extend(_trace_section(index, event))
|
| 58 |
+
else:
|
| 59 |
+
lines.append("No tool trace was recorded.")
|
| 60 |
+
|
| 61 |
+
if last_artifact:
|
| 62 |
+
lines.extend(
|
| 63 |
+
[
|
| 64 |
+
"",
|
| 65 |
+
"## Share Caption",
|
| 66 |
+
"",
|
| 67 |
+
_clean(last_artifact.get("caption") or ""),
|
| 68 |
+
]
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
return "\n".join(lines).rstrip() + "\n"
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _idea_section(index: int, idea: dict[str, Any]) -> list[str]:
|
| 75 |
+
title = _clean(idea.get("title") or f"Idea {index}")
|
| 76 |
+
pitch = _clean(idea.get("pitch"))
|
| 77 |
+
targets = [str(target) for target in idea.get("targets") or []]
|
| 78 |
+
score = idea.get("score") if isinstance(idea.get("score"), dict) else {}
|
| 79 |
+
lines = [
|
| 80 |
+
f"### {index}. {title}",
|
| 81 |
+
"",
|
| 82 |
+
f"- Pitch: {pitch or 'No pitch recorded.'}",
|
| 83 |
+
f"- Targets: {', '.join(targets) if targets else 'No specific targets'}",
|
| 84 |
+
]
|
| 85 |
+
if score:
|
| 86 |
+
lines.append(f"- Seal: {_clean(score.get('overall'))}/10 - {_clean(score.get('verdict'))}")
|
| 87 |
+
echoes = _list_of_dicts(score.get("echoes"))
|
| 88 |
+
if echoes:
|
| 89 |
+
lines.append("- Closest cited Spaces:")
|
| 90 |
+
for echo in echoes[:4]:
|
| 91 |
+
project = echo.get("project") if isinstance(echo.get("project"), dict) else {}
|
| 92 |
+
title = _clean(project.get("title") or project.get("id") or "Untitled Space")
|
| 93 |
+
url = _clean(project.get("url") or project.get("host") or "")
|
| 94 |
+
matched = ", ".join(str(term) for term in echo.get("matched_terms") or [])
|
| 95 |
+
score_text = _clean(echo.get("score"))
|
| 96 |
+
if url:
|
| 97 |
+
lines.append(f" - [{title}]({url}) - score {score_text}; matched {matched or 'no shared terms'}")
|
| 98 |
+
else:
|
| 99 |
+
lines.append(f" - {title} - score {score_text}; matched {matched or 'no shared terms'}")
|
| 100 |
+
lines.append("")
|
| 101 |
+
return lines
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _trace_section(index: int, event: dict[str, Any]) -> list[str]:
|
| 105 |
+
tools = _list_of_dicts(event.get("tools"))
|
| 106 |
+
tool_names = " -> ".join(_clean(tool.get("name")) for tool in tools if tool.get("name")) or "reply"
|
| 107 |
+
lines = [
|
| 108 |
+
f"### Turn {index}",
|
| 109 |
+
"",
|
| 110 |
+
f"- Input: {_clean(event.get('input'))}",
|
| 111 |
+
f"- Tools: {tool_names}",
|
| 112 |
+
]
|
| 113 |
+
verdict = _clean(event.get("verdict"))
|
| 114 |
+
overall = event.get("overall")
|
| 115 |
+
if verdict or overall is not None:
|
| 116 |
+
lines.append(f"- Verdict: {verdict or 'n/a'} {overall if overall is not None else ''}".rstrip())
|
| 117 |
+
response = _clean(event.get("response"))
|
| 118 |
+
if response:
|
| 119 |
+
lines.append(f"- Advisor note: {response}")
|
| 120 |
+
resolution = event.get("tool_resolution") if isinstance(event.get("tool_resolution"), dict) else {}
|
| 121 |
+
call = resolution.get("call") if isinstance(resolution.get("call"), dict) else {}
|
| 122 |
+
if call:
|
| 123 |
+
lines.append(f"- Planner call: `{_clean(call.get('name'))}`")
|
| 124 |
+
lines.append("")
|
| 125 |
+
return lines
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def _list_of_dicts(value: Any) -> list[dict[str, Any]]:
|
| 129 |
+
if not isinstance(value, list):
|
| 130 |
+
return []
|
| 131 |
+
return [item for item in value if isinstance(item, dict)]
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def _clean(value: Any) -> str:
|
| 135 |
+
if value is None:
|
| 136 |
+
return ""
|
| 137 |
+
return " ".join(str(value).split())
|
static/app.js
CHANGED
|
@@ -18,6 +18,7 @@ const verdictEl = document.querySelector("#verdict");
|
|
| 18 |
const overallEl = document.querySelector("#overall");
|
| 19 |
const exportButton = document.querySelector("#export-artifact");
|
| 20 |
const exportTraceButton = document.querySelector("#export-trace");
|
|
|
|
| 21 |
|
| 22 |
let session = {};
|
| 23 |
let clientPromise = Client.connect(window.location.origin);
|
|
@@ -49,6 +50,10 @@ exportTraceButton.addEventListener("click", async () => {
|
|
| 49 |
await exportTrace();
|
| 50 |
});
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
targetsEl.addEventListener("change", (event) => {
|
| 53 |
const target = event.target;
|
| 54 |
if (!(target instanceof HTMLInputElement) || !target.dataset.target) return;
|
|
@@ -210,6 +215,7 @@ function handleEvent(event) {
|
|
| 210 |
exportButton.disabled = false;
|
| 211 |
}
|
| 212 |
exportTraceButton.disabled = !(session.trace?.length);
|
|
|
|
| 213 |
}
|
| 214 |
}
|
| 215 |
|
|
@@ -327,7 +333,12 @@ function setCommandDisabled(disabled) {
|
|
| 327 |
document.querySelectorAll(".command-row button").forEach((button) => {
|
| 328 |
const isArtifact = button.id === "export-artifact";
|
| 329 |
const isTrace = button.id === "export-trace";
|
| 330 |
-
button.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
});
|
| 332 |
}
|
| 333 |
|
|
@@ -347,6 +358,15 @@ async function exportTrace() {
|
|
| 347 |
downloadText("hackathon-advisor-trace.jsonl", String(data || ""));
|
| 348 |
}
|
| 349 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
function exportArtifact(artifact) {
|
| 351 |
const canvas = document.createElement("canvas");
|
| 352 |
canvas.width = 1200;
|
|
@@ -402,8 +422,8 @@ function exportArtifact(artifact) {
|
|
| 402 |
link.click();
|
| 403 |
}
|
| 404 |
|
| 405 |
-
function downloadText(filename, text) {
|
| 406 |
-
const blob = new Blob([text], { type
|
| 407 |
const link = document.createElement("a");
|
| 408 |
link.download = filename;
|
| 409 |
link.href = URL.createObjectURL(blob);
|
|
|
|
| 18 |
const overallEl = document.querySelector("#overall");
|
| 19 |
const exportButton = document.querySelector("#export-artifact");
|
| 20 |
const exportTraceButton = document.querySelector("#export-trace");
|
| 21 |
+
const exportNotesButton = document.querySelector("#export-notes");
|
| 22 |
|
| 23 |
let session = {};
|
| 24 |
let clientPromise = Client.connect(window.location.origin);
|
|
|
|
| 50 |
await exportTrace();
|
| 51 |
});
|
| 52 |
|
| 53 |
+
exportNotesButton.addEventListener("click", async () => {
|
| 54 |
+
await exportNotes();
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
targetsEl.addEventListener("change", (event) => {
|
| 58 |
const target = event.target;
|
| 59 |
if (!(target instanceof HTMLInputElement) || !target.dataset.target) return;
|
|
|
|
| 215 |
exportButton.disabled = false;
|
| 216 |
}
|
| 217 |
exportTraceButton.disabled = !(session.trace?.length);
|
| 218 |
+
exportNotesButton.disabled = !(session.trace?.length);
|
| 219 |
}
|
| 220 |
}
|
| 221 |
|
|
|
|
| 333 |
document.querySelectorAll(".command-row button").forEach((button) => {
|
| 334 |
const isArtifact = button.id === "export-artifact";
|
| 335 |
const isTrace = button.id === "export-trace";
|
| 336 |
+
const isNotes = button.id === "export-notes";
|
| 337 |
+
button.disabled =
|
| 338 |
+
disabled ||
|
| 339 |
+
(isArtifact && !currentArtifact) ||
|
| 340 |
+
(isTrace && !session.trace?.length) ||
|
| 341 |
+
(isNotes && !session.trace?.length);
|
| 342 |
});
|
| 343 |
}
|
| 344 |
|
|
|
|
| 358 |
downloadText("hackathon-advisor-trace.jsonl", String(data || ""));
|
| 359 |
}
|
| 360 |
|
| 361 |
+
async function exportNotes() {
|
| 362 |
+
const client = await clientPromise;
|
| 363 |
+
const result = await client.predict("/field_notes", {
|
| 364 |
+
session_json: JSON.stringify(session),
|
| 365 |
+
});
|
| 366 |
+
const data = Array.isArray(result.data) ? result.data[0] : result.data;
|
| 367 |
+
downloadText("hackathon-advisor-field-notes.md", String(data || ""), "text/markdown;charset=utf-8");
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
function exportArtifact(artifact) {
|
| 371 |
const canvas = document.createElement("canvas");
|
| 372 |
canvas.width = 1200;
|
|
|
|
| 422 |
link.click();
|
| 423 |
}
|
| 424 |
|
| 425 |
+
function downloadText(filename, text, type = "application/jsonl;charset=utf-8") {
|
| 426 |
+
const blob = new Blob([text], { type });
|
| 427 |
const link = document.createElement("a");
|
| 428 |
link.download = filename;
|
| 429 |
link.href = URL.createObjectURL(blob);
|
static/index.html
CHANGED
|
@@ -33,6 +33,7 @@
|
|
| 33 |
<button type="button" data-command="make a build plan" title="Draft a build plan">Plan</button>
|
| 34 |
<button type="button" data-command="compare ideas" title="Compare the idea board">Rank</button>
|
| 35 |
<button type="button" id="export-trace" title="Export the tool trace" disabled>JSONL</button>
|
|
|
|
| 36 |
<button type="button" id="export-artifact" title="Export the current fate page" disabled>PNG</button>
|
| 37 |
</div>
|
| 38 |
<div id="corrections" class="corrections" aria-live="polite"></div>
|
|
|
|
| 33 |
<button type="button" data-command="make a build plan" title="Draft a build plan">Plan</button>
|
| 34 |
<button type="button" data-command="compare ideas" title="Compare the idea board">Rank</button>
|
| 35 |
<button type="button" id="export-trace" title="Export the tool trace" disabled>JSONL</button>
|
| 36 |
+
<button type="button" id="export-notes" title="Export Field Notes" disabled>Notes</button>
|
| 37 |
<button type="button" id="export-artifact" title="Export the current fate page" disabled>PNG</button>
|
| 38 |
</div>
|
| 39 |
<div id="corrections" class="corrections" aria-live="polite"></div>
|
static/styles.css
CHANGED
|
@@ -167,7 +167,7 @@ button:disabled {
|
|
| 167 |
|
| 168 |
.command-row {
|
| 169 |
display: grid;
|
| 170 |
-
grid-template-columns: repeat(
|
| 171 |
gap: 8px;
|
| 172 |
margin-top: 10px;
|
| 173 |
}
|
|
|
|
| 167 |
|
| 168 |
.command-row {
|
| 169 |
display: grid;
|
| 170 |
+
grid-template-columns: repeat(6, minmax(0, 1fr));
|
| 171 |
gap: 8px;
|
| 172 |
margin-top: 10px;
|
| 173 |
}
|
tests/test_app.py
CHANGED
|
@@ -1,6 +1,16 @@
|
|
| 1 |
import json
|
| 2 |
|
| 3 |
-
from app import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
|
| 6 |
def test_health_exposes_index_metadata() -> None:
|
|
@@ -35,6 +45,22 @@ def test_trace_artifact_endpoint_exports_jsonl() -> None:
|
|
| 35 |
assert lines[1]["type"] == "agent_turn"
|
| 36 |
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
def test_tool_contracts_endpoint_exposes_schemas() -> None:
|
| 39 |
payload = tool_contracts()
|
| 40 |
|
|
|
|
| 1 |
import json
|
| 2 |
|
| 3 |
+
from app import (
|
| 4 |
+
bootstrap,
|
| 5 |
+
engine,
|
| 6 |
+
field_notes_artifact,
|
| 7 |
+
health,
|
| 8 |
+
index,
|
| 9 |
+
runtime,
|
| 10 |
+
tool_contract_check,
|
| 11 |
+
tool_contracts,
|
| 12 |
+
trace_artifact,
|
| 13 |
+
)
|
| 14 |
|
| 15 |
|
| 16 |
def test_health_exposes_index_metadata() -> None:
|
|
|
|
| 45 |
assert lines[1]["type"] == "agent_turn"
|
| 46 |
|
| 47 |
|
| 48 |
+
def test_field_notes_endpoint_exports_markdown() -> None:
|
| 49 |
+
state = engine.turn(
|
| 50 |
+
"A local-first archive cartographer for family photos",
|
| 51 |
+
{"profile": {"skills": "frontend"}, "targets": ["Field Notes"]},
|
| 52 |
+
).state
|
| 53 |
+
state = engine.turn("make a build plan", state).state
|
| 54 |
+
|
| 55 |
+
payload = field_notes_artifact(json.dumps(state))
|
| 56 |
+
|
| 57 |
+
assert payload.startswith("# Hackathon Advisor Field Notes")
|
| 58 |
+
assert "Skills: frontend" in payload
|
| 59 |
+
assert "Targets: Field Notes" in payload
|
| 60 |
+
assert "## Turn Trace" in payload
|
| 61 |
+
assert "Record the trace and write Field Notes" in payload
|
| 62 |
+
|
| 63 |
+
|
| 64 |
def test_tool_contracts_endpoint_exposes_schemas() -> None:
|
| 65 |
payload = tool_contracts()
|
| 66 |
|
tests/test_field_notes.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
|
| 3 |
+
from hackathon_advisor.agent import AdvisorEngine
|
| 4 |
+
from hackathon_advisor.data import ProjectIndex
|
| 5 |
+
from hackathon_advisor.field_notes import build_field_notes_markdown
|
| 6 |
+
from hackathon_advisor.trace_export import trace_metadata
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def test_field_notes_markdown_contains_session_decisions() -> None:
|
| 10 |
+
index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
|
| 11 |
+
engine = AdvisorEngine(index)
|
| 12 |
+
state = {
|
| 13 |
+
"profile": {"skills": "frontend prototyping"},
|
| 14 |
+
"targets": ["Field Notes"],
|
| 15 |
+
}
|
| 16 |
+
first = engine.turn("A local-first archive cartographer for family photos", state)
|
| 17 |
+
planned = engine.turn("make a build plan", first.state)
|
| 18 |
+
|
| 19 |
+
markdown = build_field_notes_markdown(
|
| 20 |
+
planned.state,
|
| 21 |
+
{
|
| 22 |
+
**trace_metadata(index),
|
| 23 |
+
"project_count": len(index.projects),
|
| 24 |
+
},
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
assert "# Hackathon Advisor Field Notes" in markdown
|
| 28 |
+
assert "frontend prototyping" in markdown
|
| 29 |
+
assert "Targets: Field Notes" in markdown
|
| 30 |
+
assert "A local-first archive cartographer for family photos" in markdown
|
| 31 |
+
assert "## Build Plan" in markdown
|
| 32 |
+
assert "Record the trace and write Field Notes" in markdown
|
| 33 |
+
assert "Closest cited Spaces" in markdown
|
| 34 |
+
assert "Planner call: `make_plan`" in markdown
|