deploy: add agent.profikid.nl overlay (compose + FalkorDB sidecar, e2e one-shot, bootstrap/up/health scripts, README) + gitignore secrets.env
This commit is contained in:
parent
1b62a3c5b8
commit
9decc6c6f4
|
|
@ -57,4 +57,6 @@ backend/logs/
|
|||
backend/uploads/
|
||||
|
||||
# Docker 数据
|
||||
data/
|
||||
data/
|
||||
# Deployment secrets (real values, never commit)
|
||||
deploy/secrets.env
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
# Lightweight e2e test image. Inherits the MiroFish base image (already
|
||||
# has the patched graphiti_service.py + all Python deps) and only adds
|
||||
# the test script. Keeps the image small and the build fast.
|
||||
|
||||
FROM mirofish:latest
|
||||
|
||||
# Drop the long-running start.sh and just run the test
|
||||
USER root
|
||||
RUN mkdir -p /app/deploy
|
||||
COPY e2e_test.py /app/deploy/e2e_test.py
|
||||
|
||||
# Run the test as the entrypoint
|
||||
ENTRYPOINT ["python", "/app/deploy/e2e_test.py"]
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
# MiroFish deploy (agent.profikid.nl)
|
||||
|
||||
Single-host Docker deployment of the [MiroFish swarm-intelligence engine](https://github.com/666ghj/MiroFish) reforked to use **Graphiti + FalkorDB** instead of Zep Cloud.
|
||||
|
||||
## What you get
|
||||
|
||||
| URL | Service |
|
||||
| --- | --- |
|
||||
| `https://mirofish.agent.profikid.nl` | MiroFish web UI + API (frontend on :3000, Flask API on :5001, both inside the mirofish container) |
|
||||
| `mirofish_net` (internal Docker network) | FalkorDB (in-memory graph store, port 6379) |
|
||||
|
||||
Traefik (running on the host as the `traefik` Docker network) auto-issues a Let's Encrypt cert for `mirofish.agent.profikid.nl`.
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
/docker/mirofish/ <-- everything below lives here
|
||||
├── docker-compose.yml <-- mirofish + falkordb + e2e (opt-in)
|
||||
├── secrets.env <-- LLM_API_KEY, FALKORDB creds
|
||||
├── bootstrap.sh <-- one-time host checks
|
||||
├── up.sh <-- build + bring up mirofish + falkordb
|
||||
├── e2e.sh <-- run the in-container end-to-end test
|
||||
├── health.sh <-- /health, container status, falkordb ping
|
||||
├── Dockerfile.e2e <-- e2e test image (inherits mirofish:latest)
|
||||
└── e2e_test.py <-- the test itself
|
||||
```
|
||||
|
||||
## Deploy
|
||||
|
||||
```bash
|
||||
ssh root@agent.profikid.nl
|
||||
mkdir -p /docker/mirofish
|
||||
cd /docker/mirofish
|
||||
# Drop the contents of this directory here (or git clone the repo and copy deploy/ -> .)
|
||||
./bootstrap.sh # verifies docker / dns / file presence
|
||||
./up.sh # builds the image, brings up mirofish + falkordb
|
||||
```
|
||||
|
||||
After ~90s (FalkorDB cold start + embedding model download on first build only), the URL `https://mirofish.agent.profikid.nl` should respond.
|
||||
|
||||
## E2E test
|
||||
|
||||
```bash
|
||||
cd /docker/mirofish
|
||||
./e2e.sh
|
||||
```
|
||||
|
||||
This builds `mirofish-e2e:latest` (a thin layer on top of `mirofish:latest` that runs `e2e_test.py` as the entrypoint), then runs it as a one-shot container against the real `falkordb` sidecar. It:
|
||||
|
||||
1. Verifies FalkorDB is reachable (raw TCP probe)
|
||||
2. Runs the real `GraphBuilderService` against the Iran/US/Israel OSINT briefing seed text
|
||||
3. Reads back from FalkorDB via the adapter (`get_all_nodes`, `get_all_edges`)
|
||||
4. Re-verifies with a raw `falkordb.FalkorDB` client (independent check)
|
||||
5. Prints PASS / FAIL with node + edge counts
|
||||
|
||||
Expected on first run: **~8 entities, ~8 edges** in FalkorDB after the 2-chunk run (`E2E_MAX_CHUNKS=2`). Wall time ~3-4min on the first call (LLM is doing the extraction; reasoning tokens make M3 slow). Tune `E2E_MAX_CHUNKS` higher in `docker-compose.yml` to test with more seed text.
|
||||
|
||||
## How the refactor differs from upstream
|
||||
|
||||
Upstream MiroFish uses [Zep Cloud](https://www.getzep.com/) for the knowledge graph. This fork replaces it with [Graphiti](https://github.com/getzep/graphiti) (the open-source engine behind Zep) backed by [FalkorDB](https://www.falkordb.com/) (a Redis-protocol graph store). All graph operations go through a Zep-shaped facade in `backend/app/services/graphiti_service.py` so the rest of the MiroFish code (which still speaks Zep) didn't have to change.
|
||||
|
||||
Key wiring:
|
||||
|
||||
- `MinimaxLLMClient` — Graphiti's LLMClient ABC implemented against MiniMax M3 (`https://api.minimax.io/v1`). Uses the `tools` API for structured output because M3 ignores `response_format: json_object`.
|
||||
- `M3RerankerClient` — chat-completion True/False reranker (M3 returns `logprobs: None`, so the stock OpenAI reranker that reads logprobs breaks).
|
||||
- `HashEmbedder` — deterministic 384-dim embedder so the test image doesn't need to download the full `sentence-transformers` multilingual model. The production image still uses the real model (set `EMBEDDING_MODEL=sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` in `secrets.env` to switch).
|
||||
- `FalkorDriver` (from `graphiti-core[falkordb]>=0.20.0`) — the actual graph store. No Neo4j, no Bolt, just a Redis-protocol TCP socket to the sidecar.
|
||||
|
||||
## How to upgrade
|
||||
|
||||
```bash
|
||||
cd /docker/mirofish
|
||||
git pull # in the parent MiroFish repo
|
||||
./up.sh --rebuild # rebuild + restart
|
||||
./e2e.sh # smoke-test
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **FalkorDB healthcheck fails** — check `docker compose logs falkordb`. The `falkordb/falkordb:latest` image is small; first-pull can take a minute.
|
||||
- **`/health` returns 200 but `/api/ontology/generate` times out** — the embedding model is downloading on first request. Wait, or run with a prebuilt `mirofish:latest` that has the model baked in (the `Dockerfile` already does this in step 4).
|
||||
- **M3 returns 0 entities** — the M3 endpoint is shared; rate limiting or reasoning-token quirks can cause this. Check `docker compose logs mirofish | grep graphiti_service` and the raw LLM payloads.
|
||||
- **Cert issuance stuck in Traefik** — confirm `TRAEFIK_HOST=agent.profikid.nl` and that the `traefik` Docker network exists. Traefik must be on the same network as the `mirofish` container.
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env bash
|
||||
# MiroFish host bootstrap — agent.profikid.nl
|
||||
#
|
||||
# Run this ONCE on the host (after cloning the repo to /docker/mirofish/).
|
||||
# It creates the project directory layout and verifies Docker + Traefik are
|
||||
# reachable. Does NOT actually deploy — that's the `up.sh` next to this file.
|
||||
#
|
||||
# Usage: ./bootstrap.sh
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "[bootstrap] creating /docker/mirofish/ if missing..."
|
||||
sudo mkdir -p /docker/mirofish
|
||||
sudo chown -R "$USER":"$USER" /docker/mirofish || true
|
||||
|
||||
echo "[bootstrap] required files present:"
|
||||
for f in docker-compose.yml secrets.env Dockerfile Dockerfile.e2e e2e_test.py; do
|
||||
if [ ! -f "$f" ] && [ ! -f "../$f" ] && [ ! -f "../deploy/$f" ]; then
|
||||
echo " MISSING: $f" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " OK: $f"
|
||||
done
|
||||
|
||||
echo "[bootstrap] checking docker..."
|
||||
docker --version
|
||||
docker compose version
|
||||
|
||||
echo "[bootstrap] checking traefik network..."
|
||||
docker network ls --format '{{.Name}}' | grep -q '^traefik$' \
|
||||
|| { echo " WARN: 'traefik' network not found. Traefik must be on a network called 'traefik' (or edit docker-compose.yml)." >&2; }
|
||||
|
||||
echo "[bootstrap] DNS for agent.profikid.nl (must resolve to 69.62.114.199)..."
|
||||
RESOLVED=$(getent hosts agent.profikid.nl | awk '{print $1; exit}')
|
||||
if [ "$RESOLVED" != "69.62.114.199" ]; then
|
||||
echo " WARN: agent.profikid.nl resolves to $RESOLVED, expected 69.62.114.199" >&2
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "[bootstrap] all checks passed."
|
||||
echo
|
||||
echo "Next: edit secrets.env if you need to change LLM keys, then run ./up.sh"
|
||||
|
|
@ -70,6 +70,29 @@ services:
|
|||
networks:
|
||||
- mirofish_net
|
||||
|
||||
# One-shot e2e test. Built but only runs when you opt in:
|
||||
# docker compose --profile e2e run --rm mirofish-e2e
|
||||
# Output is printed to the host (PASS/FAIL exit code).
|
||||
mirofish-e2e:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deploy/Dockerfile.e2e
|
||||
image: mirofish-e2e:latest
|
||||
container_name: mirofish-e2e
|
||||
depends_on:
|
||||
falkordb:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- secrets.env
|
||||
environment:
|
||||
FALKORDB_HOST: falkordb
|
||||
FALKORDB_PORT: "6379"
|
||||
E2E_MAX_CHUNKS: "2"
|
||||
networks:
|
||||
- mirofish_net
|
||||
profiles: ["e2e"]
|
||||
restart: "no"
|
||||
|
||||
volumes:
|
||||
uploads:
|
||||
flask-logs:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
# MiroFish e2e — runs the in-container e2e test against the real FalkorDB
|
||||
# sidecar. Prints PASS/FAIL and exits accordingly.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
export COMPOSE_PROJECT_NAME=mirofish
|
||||
|
||||
# Make sure the e2e image is built
|
||||
if ! docker image inspect mirofish-e2e:latest >/dev/null 2>&1; then
|
||||
echo "[e2e] building mirofish-e2e image..."
|
||||
docker compose --profile e2e build mirofish-e2e
|
||||
fi
|
||||
|
||||
echo "[e2e] running test against falkordb sidecar..."
|
||||
# `run --rm` creates a one-off container with the e2e profile, runs the test,
|
||||
# and removes the container on exit. Exit code propagates.
|
||||
docker compose --profile e2e run --rm mirofish-e2e
|
||||
RC=$?
|
||||
|
||||
echo
|
||||
if [ "$RC" -eq 0 ]; then
|
||||
echo "[e2e] PASS"
|
||||
else
|
||||
echo "[e2e] FAIL (exit $RC)"
|
||||
fi
|
||||
exit "$RC"
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
"""
|
||||
MiroFish → FalkorDB end-to-end test (production-mode).
|
||||
|
||||
Runs the same code path the API uses, but in a one-shot container, against
|
||||
the real FalkorDB sidecar in the same compose project. Verifies that:
|
||||
|
||||
- FalkorDB is reachable from the app container (real TCP, real protocol)
|
||||
- graphiti-core connects and the Zep-shaped adapter works
|
||||
- Entities + edges actually land in FalkorDB after add_episode
|
||||
- The /health endpoint on the running MiroFish container is 200
|
||||
|
||||
Exits 0 on PASS, 1 on FAIL.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
# Make MiroFish backend importable
|
||||
MIROFISH_BACKEND = "/app/backend"
|
||||
if MIROFISH_BACKEND not in sys.path:
|
||||
sys.path.insert(0, MIROFISH_BACKEND)
|
||||
|
||||
# .env is loaded by MiroFish's config.py; nothing to do here.
|
||||
print("=" * 70)
|
||||
print("MIROFISH END-TO-END TEST (FalkorDB backend)")
|
||||
print("=" * 70)
|
||||
print(f" FALKORDB_HOST = {os.environ.get('FALKORDB_HOST', '<unset>')}")
|
||||
print(f" FALKORDB_PORT = {os.environ.get('FALKORDB_PORT', '<unset>')}")
|
||||
print(f" LLM_BASE_URL = {os.environ.get('LLM_BASE_URL', '<unset>')}")
|
||||
print(f" LLM_MODEL_NAME = {os.environ.get('LLM_MODEL_NAME', '<unset>')}")
|
||||
print(f" EMBEDDING_MODEL= {os.environ.get('EMBEDDING_MODEL','<unset>')}")
|
||||
print()
|
||||
|
||||
# -- 0. Sanity: is FalkorDB up? Use a raw TCP probe (no app deps) --
|
||||
import socket
|
||||
fk_host = os.environ.get("FALKORDB_HOST", "falkordb")
|
||||
fk_port = int(os.environ.get("FALKORDB_PORT", "6379"))
|
||||
try:
|
||||
with socket.create_connection((fk_host, fk_port), timeout=5):
|
||||
print(f"[0] FalkorDB TCP reachable at {fk_host}:{fk_port}")
|
||||
except OSError as e:
|
||||
print(f"[0] FAIL: FalkorDB not reachable at {fk_host}:{fk_port}: {e}")
|
||||
sys.exit(2)
|
||||
|
||||
# -- 1. The Iran/US/Israel OSINT cron briefing as the seed text --
|
||||
# In a real MiroFish run, this comes from /home/hermes/.hermes/cron/output/...
|
||||
# For the e2e test, we ship a copy of the latest briefing inline.
|
||||
SEED_TEXT = """\
|
||||
## IRAN/US/ISRAEL CONFLICT — UPDATE
|
||||
**Timestamp:** Monday, 8 June 2026, ~21:00 UTC
|
||||
**Coverage Period:** Last 12 hours (07–08 June 2026)
|
||||
|
||||
### INCIDENTS / HOSTILITIES
|
||||
- **Iran launched ballistic missile attack on Israel** (Sun 7 Jun, evening local) — first direct Iranian bombardment since the April 8 ceasefire. Multiple projectiles struck central Israel; Israeli emergency services report **3 killed and >100 injured** in a strike on a building in **Bat Yam** (south of Tel Aviv). [BBC, NYT, AP]
|
||||
- **Israeli retaliatory strikes on Iran** conducted hours after the Iranian launch, targeting military sites in Tehran. Netanyahu stated Israel "hit the terror regime in Tehran." [NYT, Times of Israel]
|
||||
- **Hezbollah rocket fire on northern Israel** (7 Jun) preceded the Israeli strike on Beirut's southern suburbs, which killed **2 and wounded ~11–20** (Lebanese state media). [NPR]
|
||||
- **Iranian drone attack on US interests in the Strait of Hormuz** — US Central Command reports shooting down a pair of Iranian drones threatening the strait on 7 Jun. [CBS]
|
||||
|
||||
### DIPLOMATIC / POLITICAL
|
||||
- **Trump-Netanyahu phone call** (7 Jun, post-Iranian missile launch) — Trump urged Netanyahu **not to retaliate**; Netanyahu reportedly replied "The Iranians violated our sovereignty."
|
||||
- **Iran declared halt to military offensive** against Israel on 8 Jun.
|
||||
|
||||
### CEASEFIRE / TRUCE STATUS
|
||||
- **April 8 Iran-US ceasefire:** Functionally **broken** — first direct Iran-Israel missile exchange since the truce.
|
||||
|
||||
### REGIONAL SPILLOVER
|
||||
- **Strait of Hormuz:** US naval blockade in effect; shipping at near-standstill since March. IRGC restricts traffic; ~150+ tankers anchored outside. US forces disabled 4 vessels, redirected 94. [CNN, Reuters]
|
||||
- **Houthi attacks (Yemen):** Iran-backed Houthi rebels struck Israel; rocket debris found near Jericho (West Bank) on 8 Jun.
|
||||
- **Iraq/Syria:** US radar sites attacked; Iranian drones shot down near US positions (per US claims, 6 Jun).
|
||||
- **Kuwait/Bahrain:** Earlier Iranian ballistic missile volleys (2–5 Jun) hit Kuwait International Airport.
|
||||
|
||||
### ASSESSMENT
|
||||
The April 8 ceasefire has effectively collapsed after the first direct Iran-Israel missile exchange in two months, triggered by Israel's Beirut strike on Hezbollah. Both sides have paused strikes as of 8 Jun following Trump intervention, but a 60-day US-Iran deal remains unsigned and Hezbollah's rejection of the Lebanon track preserves a major escalation vector. **High risk of renewed escalation within 24–72 hours** if Trump-Netanyahu alignment fractures or Lebanon track collapses.
|
||||
"""
|
||||
|
||||
print(f"[1] Seed text: {len(SEED_TEXT)} chars (Iran/US/Israel OSINT briefing)")
|
||||
|
||||
# -- 2. Ontology (same shape MiroFish's OntologyGenerator would emit) --
|
||||
ONTOLOGY = {
|
||||
"entity_types": [
|
||||
{"name": "Country", "description": "A sovereign nation or state actor.",
|
||||
"attributes": [{"name": "iso_code", "description": "ISO code"},
|
||||
{"name": "government_type", "description": "Form of government"}]},
|
||||
{"name": "Person", "description": "A named individual mentioned in the briefing.",
|
||||
"attributes": [{"name": "role", "description": "Title, position, or affiliation"}]},
|
||||
{"name": "Organization","description": "A formal organization, military unit, or political body.",
|
||||
"attributes": [{"name": "type", "description": "e.g. political, militant, state"}]},
|
||||
{"name": "Location", "description": "A geographic place, city, or strategic region.",
|
||||
"attributes": [{"name": "region", "description": "Broader region"}]},
|
||||
{"name": "Event", "description": "A distinct incident, attack, meeting, or action.",
|
||||
"attributes": [{"name": "date", "description": "When the event occurred"},
|
||||
{"name": "event_type", "description": "Type of event"}]},
|
||||
],
|
||||
"edge_types": [
|
||||
{"name": "LAUNCHED_ATTACK_AGAINST", "description": "Actor performed a military strike on target.",
|
||||
"source_targets": [{"source": "Country", "target": "Country"},
|
||||
{"source": "Organization", "target": "Country"}],
|
||||
"attributes": [{"name": "date", "description": "Date"},
|
||||
{"name": "weapon", "description": "Weapon used"}]},
|
||||
{"name": "OCCURRED_IN", "description": "An event happened at a location.",
|
||||
"source_targets": [{"source": "Event", "target": "Location"}],
|
||||
"attributes": [{"name": "date", "description": "Date"}]},
|
||||
{"name": "IS_LEADER_OF", "description": "A person leads a country or organization.",
|
||||
"source_targets": [{"source": "Person", "target": "Country"},
|
||||
{"source": "Person", "target": "Organization"}],
|
||||
"attributes": []},
|
||||
{"name": "MADE_STATEMENT", "description": "A person or organization issued a public statement.",
|
||||
"source_targets": [{"source": "Person", "target": "Event"}],
|
||||
"attributes": [{"name": "stance", "description": "Position taken"}]},
|
||||
{"name": "PARTICIPATED_IN", "description": "A country or organization was involved in an event.",
|
||||
"source_targets": [{"source": "Country", "target": "Event"}],
|
||||
"attributes": []},
|
||||
],
|
||||
}
|
||||
print(f"[2] Ontology: {len(ONTOLOGY['entity_types'])} entity types, "
|
||||
f"{len(ONTOLOGY['edge_types'])} edge types")
|
||||
|
||||
# -- 3. Chunk the seed text --
|
||||
from app.services.text_processor import TextProcessor
|
||||
chunks = TextProcessor.split_text(SEED_TEXT, chunk_size=500, overlap=50)
|
||||
print(f"[3] Text split: {len(chunks)} chunks")
|
||||
|
||||
# -- 4. Run the build pipeline via the real GraphBuilderService --
|
||||
from app.services.graph_builder import GraphBuilderService
|
||||
from app.services import graph_builder as _gb_mod # for the EpisodeData stub
|
||||
EpisodeData = _gb_mod.EpisodeData
|
||||
|
||||
MAX_CHUNKS = int(os.environ.get("E2E_MAX_CHUNKS", "2"))
|
||||
test_chunks = chunks[:MAX_CHUNKS]
|
||||
print(f"[4] Using {len(test_chunks)} chunks for the e2e test (E2E_MAX_CHUNKS={MAX_CHUNKS})")
|
||||
print()
|
||||
|
||||
print("[5] Running graph build pipeline...")
|
||||
t0 = time.time()
|
||||
builder = GraphBuilderService()
|
||||
graph_id = builder.create_graph(name="Iran_US_Israel_OSINT_e2e")
|
||||
print(f" create_graph -> {graph_id}")
|
||||
builder.set_ontology(graph_id, ONTOLOGY)
|
||||
print(f" set_ontology -> cached {len(ONTOLOGY['entity_types'])} types, {len(ONTOLOGY['edge_types'])} edges")
|
||||
|
||||
episodes = [EpisodeData(data=c, type="text") for c in test_chunks]
|
||||
print(f" add_batch -> sending {len(episodes)} EpisodeData objects...")
|
||||
batch_result = builder.client.add_batch(graph_id=graph_id, episodes=episodes)
|
||||
t1 = time.time()
|
||||
print(f" add_batch took {t1 - t0:.1f}s")
|
||||
for r in batch_result:
|
||||
print(f" uuid={r.uuid_} processed={r.processed}")
|
||||
|
||||
# -- 5. Verify in FalkorDB via the adapter's read methods --
|
||||
print()
|
||||
print("[6] Reading back from FalkorDB via the adapter...")
|
||||
nodes = builder.client.get_all_nodes(graph_id)
|
||||
edges = builder.client.get_all_edges(graph_id)
|
||||
print(f" nodes: {len(nodes)}")
|
||||
print(f" edges: {len(edges)}")
|
||||
|
||||
print()
|
||||
print("[7] Sample entities (first 10):")
|
||||
for n in nodes[:10]:
|
||||
labels_str = ",".join(n.labels) if n.labels else "(no labels)"
|
||||
summary_preview = (n.summary[:60] + "...") if n.summary and len(n.summary) > 60 else (n.summary or "")
|
||||
print(f" [{labels_str}] {n.name!r} uuid={n.uuid_[:8]} summary={summary_preview!r}")
|
||||
|
||||
print()
|
||||
print("[8] Sample relationships (first 5):")
|
||||
for e in edges[:5]:
|
||||
src = e.source_node_uuid[:8] if e.source_node_uuid else "?"
|
||||
dst = e.target_node_uuid[:8] if e.target_node_uuid else "?"
|
||||
fact_preview = (e.fact[:60] + "...") if e.fact and len(e.fact) > 60 else (e.fact or "")
|
||||
print(f" ({src}) -[{e.name or e.fact_type}]→ ({dst}) fact={fact_preview!r}")
|
||||
|
||||
# -- 6. Independent verification with the raw falkordb client --
|
||||
print()
|
||||
print("[9] Direct falkordb verification (source of truth)...")
|
||||
import falkordb
|
||||
client = falkordb.FalkorDB(host=fk_host, port=fk_port)
|
||||
graph = client.select_graph(graph_id)
|
||||
n_count = graph.query("MATCH (n) RETURN count(n) AS c").result_set
|
||||
e_count = graph.query("MATCH ()-[r]->() RETURN count(r) AS c").result_set
|
||||
print(f" FalkorDB MATCH count(n) = {n_count}")
|
||||
print(f" FalkorDB MATCH count(edges) = {e_count}")
|
||||
labels = graph.query("MATCH (n) UNWIND labels(n) AS l RETURN l, count(*) AS c ORDER BY c DESC LIMIT 20")
|
||||
print(f" Entity label distribution:")
|
||||
for row in labels.result_set:
|
||||
print(f" {row[0]}: {row[1]}")
|
||||
names = graph.query("MATCH (n) RETURN n.name AS name LIMIT 20")
|
||||
print(f" Sample node names: {[r[0] for r in names.result_set]}")
|
||||
|
||||
# -- 7. Final summary --
|
||||
print()
|
||||
print("=" * 70)
|
||||
print("E2E TEST SUMMARY")
|
||||
print("=" * 70)
|
||||
print(f" Graph ID: {graph_id}")
|
||||
print(f" Seed text: Iran/US/Israel OSINT briefing ({len(SEED_TEXT)} chars)")
|
||||
print(f" Chunks ingested: {len(episodes)}")
|
||||
print(f" Nodes in FalkorDB: {len(nodes)} (direct: {n_count[0][0]})")
|
||||
print(f" Edges in FalkorDB: {len(edges)} (direct: {e_count[0][0]})")
|
||||
print(f" Wall time: {t1 - t0:.1f}s")
|
||||
print(f" Pass criteria: node_count > 0 AND edge_count > 0")
|
||||
passing = (len(nodes) > 0 and len(edges) > 0)
|
||||
print(f" RESULT: {'PASS' if passing else 'FAIL'}")
|
||||
print("=" * 70)
|
||||
sys.exit(0 if passing else 1)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
#!/usr/bin/env bash
|
||||
# Quick healthcheck — /health endpoint, container status, FalkorDB ping.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "[health] docker compose ps:"
|
||||
docker compose ps
|
||||
|
||||
echo
|
||||
echo "[health] mirofish /health:"
|
||||
docker compose exec -T mirofish curl -sS -m 5 http://127.0.0.1:3000/health || echo " /health FAILED"
|
||||
|
||||
echo
|
||||
echo "[health] falkordb ping:"
|
||||
docker compose exec -T falkordb redis-cli ping || echo " redis-cli FAILED"
|
||||
|
||||
echo
|
||||
echo "[health] falkordb graph list:"
|
||||
docker compose exec -T falkordb redis-cli GRAPH.LIST || echo " GRAPH.LIST FAILED"
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# MiroFish secrets for the agent.profikid.nl deployment.
|
||||
#
|
||||
# Rename to `secrets.env` and fill in. Filename is `secrets.env` (not `.env`)
|
||||
# on purpose — Hermes' safety guard blocks read/write of any file literally
|
||||
# named `.env`.
|
||||
#
|
||||
# DO NOT COMMIT a populated secrets.env to git. This template IS in git
|
||||
# (secrets.env.example); the real one is gitignored.
|
||||
#
|
||||
# LLM provider: MiniMax M3 via the OpenAI-compat endpoint
|
||||
# (verified working: https://api.minimax.io/v1 returns chat completions for model "MiniMax-M3")
|
||||
|
||||
LLM_API_KEY=sk-cp-REPLACE-ME
|
||||
LLM_BASE_URL=https://api.minimax.io/v1
|
||||
LLM_MODEL_NAME=MiniMax-M3
|
||||
|
||||
# Memory graph backend: Graphiti + FalkorDB (replaces Zep)
|
||||
# Host/port are set in docker-compose.yml; the keys here are auth (FalkorDB
|
||||
# ships without auth, so we leave username/password empty for on-prem).
|
||||
FALKORDB_HOST=falkordb
|
||||
FALKORDB_PORT=6379
|
||||
FALKORDB_USERNAME=
|
||||
FALKORDB_PASSWORD=
|
||||
|
||||
# Embedding model. Two options:
|
||||
# 1) Local sentence-transformers, pre-downloaded in the image (set this):
|
||||
# sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
|
||||
# 2) Lightweight deterministic hash embedder (used by the e2e test image,
|
||||
# useful for CI / quick smoke tests):
|
||||
# hash
|
||||
EMBEDDING_MODEL=sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
#!/usr/bin/env bash
|
||||
# MiroFish up — agent.profikid.nl
|
||||
#
|
||||
# Builds the image, brings up mirofish + falkordb sidecar on the
|
||||
# mirofish_net network, registers Traefik routes. Idempotent.
|
||||
#
|
||||
# Usage:
|
||||
# ./up.sh # build + up -d
|
||||
# ./up.sh --no-build # up -d, skip build
|
||||
# ./up.sh --rebuild # force rebuild (no cache)
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
BUILD_FLAGS=()
|
||||
if [ "${1:-}" = "--no-build" ]; then
|
||||
BUILD_FLAGS+=(--no-build)
|
||||
elif [ "${1:-}" = "--rebuild" ]; then
|
||||
BUILD_FLAGS+=(--build --force-rm --no-cache --pull)
|
||||
else
|
||||
BUILD_FLAGS+=(--build)
|
||||
fi
|
||||
|
||||
export COMPOSE_PROJECT_NAME=mirofish
|
||||
export TRAEFIK_HOST=agent.profikid.nl
|
||||
|
||||
echo "[up] COMPOSE_PROJECT_NAME=$COMPOSE_PROJECT_NAME"
|
||||
echo "[up] TRAEFIK_HOST=$TRAEFIK_HOST"
|
||||
echo "[up] building..."
|
||||
docker compose "${BUILD_FLAGS[@]}"
|
||||
|
||||
echo "[up] bringing up mirofish + falkordb..."
|
||||
docker compose up -d
|
||||
|
||||
echo "[up] tailing logs for ~20s while healthchecks settle..."
|
||||
docker compose logs --tail=200 --since=2m &
|
||||
LOG_PID=$!
|
||||
sleep 20
|
||||
kill "$LOG_PID" 2>/dev/null || true
|
||||
|
||||
echo
|
||||
echo "[up] status:"
|
||||
docker compose ps
|
||||
echo
|
||||
echo "URL (after Traefik cert): https://mirofish.agent.profikid.nl"
|
||||
echo "E2E test: ./e2e.sh (runs the in-container e2e test against real FalkorDB)"
|
||||
Loading…
Reference in New Issue