feat: add AI research chatbot with confirmed business tools
This commit is contained in:
@@ -89,6 +89,7 @@ asyncio.run(seed())
|
||||
"PATCH",
|
||||
{
|
||||
"note": "persistent local note",
|
||||
"version": 1,
|
||||
"tags": ["docker-verified"],
|
||||
"state": "candidate",
|
||||
"favorite": True,
|
||||
@@ -105,6 +106,78 @@ asyncio.run(seed())
|
||||
assert detail["sharpe"] == 3 and detail["research"]["state"] == "candidate"
|
||||
assert detail["research"]["tags"] == ["docker-verified"]
|
||||
print("PASS: container replacement preserves session and data; snapshot update preserves research")
|
||||
|
||||
# This synthetic service runs inside the disposable backend container. No real provider is used.
|
||||
model_server = Path(__file__).with_name("model_protocol.py").read_text()
|
||||
|
||||
def start_model():
|
||||
run(["exec", "-d", "-T", "backend", "python", "-c", model_server])
|
||||
run(
|
||||
[
|
||||
"exec",
|
||||
"-T",
|
||||
"backend",
|
||||
"python",
|
||||
"-c",
|
||||
"import socket,time\nfor _ in range(40):\n try:\n socket.create_connection(('127.0.0.1',19010),timeout=1).close();break\n except OSError: time.sleep(.1)\nelse: raise RuntimeError('Mock model did not start')",
|
||||
]
|
||||
)
|
||||
|
||||
start_model()
|
||||
for protocol in ("chat_completions", "responses"):
|
||||
config = {"base_url": "http://127.0.0.1:19010/v1", "model": "mock-model", "protocol": protocol}
|
||||
json.load(request("/api/v1/ai/settings", "PUT", config | {"api_key": "synthetic-model-key"}))
|
||||
tested = json.load(request("/api/v1/ai/settings/test", "POST"))
|
||||
assert tested["ready"], tested
|
||||
json.load(request("/api/v1/ai/settings", "PUT", config | {"enabled": True}))
|
||||
conversation = json.load(request("/api/v1/ai/conversations", "POST"))["id"]
|
||||
started = time.monotonic()
|
||||
stream = request(
|
||||
f"/api/v1/ai/conversations/{conversation}/runs", "POST", {"request_id": "stream", "message": "SLOW"}
|
||||
)
|
||||
run_id = stream.headers["X-AI-Run-ID"]
|
||||
assert stream.headers["x-vercel-ai-ui-message-stream"] == "v1"
|
||||
while b'"text-delta"' not in stream.readline():
|
||||
assert time.monotonic() - started < 8
|
||||
assert json.load(request(f"/api/v1/ai/runs/{run_id}"))["status"] == "running"
|
||||
stream.close()
|
||||
for _ in range(40):
|
||||
if json.load(request(f"/api/v1/ai/runs/{run_id}"))["status"] == "completed":
|
||||
break
|
||||
time.sleep(0.25)
|
||||
else:
|
||||
raise AssertionError("Disconnected execution did not complete")
|
||||
stream = request(
|
||||
f"/api/v1/ai/conversations/{conversation}/runs",
|
||||
"POST",
|
||||
{"request_id": "write", "message": "修改研究记录"},
|
||||
)
|
||||
run_id = stream.headers["X-AI-Run-ID"]
|
||||
stream.read()
|
||||
pending = json.load(request(f"/api/v1/ai/runs/{run_id}"))
|
||||
assert pending["status"] == "waiting_approval", pending
|
||||
assert (
|
||||
json.load(request("/api/v1/alphas/DOCKER_ACCEPTANCE"))["research"]["note"] == "persistent local note"
|
||||
)
|
||||
run(["up", "-d", "--force-recreate", "--wait"])
|
||||
start_model()
|
||||
assert json.load(request("/api/v1/ai/settings"))["ready"]
|
||||
approval = pending["tools"][0]["id"]
|
||||
for _ in range(2):
|
||||
request(f"/api/v1/ai/approvals/{approval}/decision", "POST", {"approved": True}).read()
|
||||
saved = json.load(request("/api/v1/alphas/DOCKER_ACCEPTANCE"))["research"]
|
||||
assert saved["note"] == "AI verified note" and saved["version"] == 3
|
||||
try:
|
||||
request("/api/v1/alphas/DOCKER_ACCEPTANCE/research", "PATCH", {"version": 2, "note": "stale"})
|
||||
raise AssertionError("A stale edit overwrote AI research")
|
||||
except urllib.error.HTTPError as error:
|
||||
assert error.code == 409
|
||||
print(
|
||||
"PASS: both real SDK protocols over mock HTTP; Caddy streams before completion; disconnect recovery"
|
||||
)
|
||||
print(
|
||||
"PASS: pending approval survives container replacement; duplicate confirmation writes once; PostgreSQL version conflict"
|
||||
)
|
||||
dump = run(["exec", "-T", "db", "pg_dump", "-U", "wq", "-d", "wq", "-Fc", "--no-owner"])
|
||||
backup = Path(".local/docker-acceptance.dump")
|
||||
fd = os.open(backup, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
@@ -147,8 +220,57 @@ asyncio.run(seed())
|
||||
.decode()
|
||||
.strip()
|
||||
)
|
||||
assert rows == "persistent local note|candidate"
|
||||
assert rows == "AI verified note|candidate"
|
||||
ai_rows = (
|
||||
run(
|
||||
[
|
||||
"exec",
|
||||
"-T",
|
||||
"db",
|
||||
"psql",
|
||||
"-U",
|
||||
"wq",
|
||||
"-d",
|
||||
"wq_acceptance_restore",
|
||||
"-At",
|
||||
"-c",
|
||||
"SELECT (SELECT count(*) FROM ai_conversations), (SELECT count(*) FROM ai_runs), (SELECT count(*) FROM ai_tool_calls), (SELECT count(*) FROM ai_settings WHERE api_key_encrypted IS NOT NULL)",
|
||||
]
|
||||
)
|
||||
.decode()
|
||||
.strip()
|
||||
)
|
||||
assert ai_rows == "1|2|1|1", ai_rows
|
||||
print("PASS: PostgreSQL custom-format backup restores records into independent database")
|
||||
# Downgrade only the scratch restore database, then verify upgrading existing 0001 research.
|
||||
migration_env = (
|
||||
"DATABASE_URL=postgresql+asyncpg://wq:"
|
||||
+ values["POSTGRES_PASSWORD"]
|
||||
+ "@db:5432/wq_acceptance_restore"
|
||||
)
|
||||
for command in (["downgrade", "0001"], ["upgrade", "head"], ["check"]):
|
||||
run(["exec", "-T", "-e", migration_env, "backend", "alembic", *command])
|
||||
versioned = (
|
||||
run(
|
||||
[
|
||||
"exec",
|
||||
"-T",
|
||||
"db",
|
||||
"psql",
|
||||
"-U",
|
||||
"wq",
|
||||
"-d",
|
||||
"wq_acceptance_restore",
|
||||
"-At",
|
||||
"-c",
|
||||
"SELECT note || '|' || version FROM research WHERE alpha_id='DOCKER_ACCEPTANCE'",
|
||||
]
|
||||
)
|
||||
.decode()
|
||||
.strip()
|
||||
)
|
||||
assert versioned == "AI verified note|1", versioned
|
||||
print("PASS: existing 0001 research upgrades with version 1 and unchanged content; Alembic model parity")
|
||||
config = json.loads(run(["-f", "compose.public.yaml", "config", "--format", "json"]))
|
||||
assert config["services"]["backend"]["environment"]["COOKIE_SECURE"] == "true"
|
||||
assert config["services"]["backend"]["environment"]["PUBLIC_ORIGIN"].startswith("https://")
|
||||
|
||||
Reference in New Issue
Block a user