Skip to content
This repository was archived by the owner on Aug 25, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
356 changes: 341 additions & 15 deletions bin/fm-cognee-lookup.sh
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
#!/usr/bin/env bash
# Local dry-run wrapper for future Cognee lookup integration.
# Local and live read-only wrapper for Cognee lookup integration.
#
# This script deliberately does not call Cognee. It accepts a local answer
# fixture, treats it as an untrusted hint, and asks the local manifest checker to
# prove whether any cited source can be reopened and checksum-verified.
# Dry-run mode accepts a local answer fixture. Live mode calls only the read-only
# Cognee search endpoint, treats the response as an untrusted hint, and asks the
# local manifest checker to prove whether any cited source can be reopened and
# checksum-verified.
set -eu

usage() {
cat >&2 <<'USAGE'
usage: fm-cognee-lookup.sh --dry-run --query <text> [--manifest <manifest.tsv> --answer-file <answer.txt>]
usage: fm-cognee-lookup.sh [--dry-run] --query <text> [--manifest <manifest.tsv|manifest.jsonl> --answer-file <answer.txt>]
fm-cognee-lookup.sh <query text>

No live mode exists yet. Without --dry-run this command fails closed before any
network, environment, MCP, or config access can happen.
Live mode uses only already-exported environment variables:
COGNEE_BASE_URL
COGNEE_API_KEY
FM_COGNEE_MANIFEST or --manifest

It can be used through:
FM_COGNEE_LOOKUP_CMD=/absolute/path/to/bin/fm-cognee-lookup.sh

Live mode calls only POST /api/v1/search and never creates datasets, imports,
cognifies, deletes, syncs, mutates config, mutates MCP, or writes env files.
USAGE
}

Expand All @@ -27,6 +37,215 @@ DRY_RUN=false
QUERY=
MANIFEST=
ANSWER_FILE=
POSITIONAL=()

safe_label() {
printf '%s' "$1" | sed -E 's/[^A-Za-z0-9_.:-]+/_/g; s/^_+//; s/_+$//' | cut -c 1-120
}

dataset_alias() {
printf '%s' "${FM_COGNEE_DATASET_ALIAS:-${COGNEE_DATASET_ALIAS:-firstmate-curated-memory-0629}}"
}

dataset_id_hash() {
if [ -n "${COGNEE_DATASET_ID:-}" ]; then
printf 'sha256:%s' "$(printf '%s' "$COGNEE_DATASET_ID" | sha256sum | awk '{print $1}')"
fi
}

live_telemetry_log() {
local status=$1 error_class=$2 http_status=$3 retryable=$4 retry_count=$5 latency_ms=$6 parsed_source_count=$7 verification_outcome=$8
local telemetry_file dataset_alias_value dataset_id_hash_value
telemetry_file=${FM_COGNEE_TELEMETRY_FILE:-$(fm_cognee_telemetry_default_path)}
dataset_alias_value=$(dataset_alias)
dataset_id_hash_value=$(dataset_id_hash)
(
set +e
mkdir -p "$(dirname "$telemetry_file")" >/dev/null 2>&1 || exit 0
FM_COGNEE_LIVE_TELEMETRY_FILE=$telemetry_file \
FM_COGNEE_LIVE_STATUS=$(safe_label "$status") \
FM_COGNEE_LIVE_ERROR=$(safe_label "$error_class") \
FM_COGNEE_LIVE_HTTP_STATUS=$http_status \
FM_COGNEE_LIVE_RETRYABLE=$retryable \
FM_COGNEE_LIVE_RETRY_COUNT=$retry_count \
FM_COGNEE_LIVE_LATENCY_MS=$latency_ms \
FM_COGNEE_LIVE_PARSED_SOURCE_COUNT=$parsed_source_count \
FM_COGNEE_LIVE_VERIFICATION=$(safe_label "$verification_outcome") \
FM_COGNEE_LIVE_DATASET_ALIAS=$(safe_label "$dataset_alias_value") \
FM_COGNEE_LIVE_DATASET_ID_HASH=$dataset_id_hash_value \
FM_COGNEE_LIVE_SEARCH_TYPE=$(safe_label "${FM_COGNEE_SEARCH_TYPE:-RAG_COMPLETION}") \
FM_COGNEE_LIVE_TOP_K=${FM_COGNEE_TOP_K:-8} \
python3 - <<'PY' >/dev/null 2>&1
import datetime as dt
import json
import os
from pathlib import Path


def integer(name, default=0):
try:
return max(int(os.environ.get(name, "") or default), 0)
except ValueError:
return default


def maybe_int(name):
value = os.environ.get(name, "")
if value == "":
return None
try:
return int(value)
except ValueError:
return None


def boolean(name):
return (os.environ.get(name, "") or "").lower() == "true"


now = dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
event = {
"schema_version": "cognee_live_lookup.v1",
"ts_utc": now,
"event_type": "api_attempt",
"operation_name": "cognee_lookup",
"mode": "live",
"dataset": {
"dataset_alias": os.environ.get("FM_COGNEE_LIVE_DATASET_ALIAS") or "unknown",
"dataset_id_hash": os.environ.get("FM_COGNEE_LIVE_DATASET_ID_HASH") or None,
},
"operation": {
"operation_name": "search",
"endpoint_template": "/api/v1/search",
"http_method": "POST",
"mutates_remote": False,
"search_type": os.environ.get("FM_COGNEE_LIVE_SEARCH_TYPE") or "RAG_COMPLETION",
"topK": integer("FM_COGNEE_LIVE_TOP_K", 8),
},
"status": {
"status": os.environ.get("FM_COGNEE_LIVE_STATUS") or "unknown",
"error_class": os.environ.get("FM_COGNEE_LIVE_ERROR") or "none",
"http_status": maybe_int("FM_COGNEE_LIVE_HTTP_STATUS"),
"retryable": boolean("FM_COGNEE_LIVE_RETRYABLE"),
},
"attempt": {
"retry_count": integer("FM_COGNEE_LIVE_RETRY_COUNT", 0),
},
"latency": {
"duration_ms": integer("FM_COGNEE_LIVE_LATENCY_MS", 0),
},
"results": {
"parsed_source_count": integer("FM_COGNEE_LIVE_PARSED_SOURCE_COUNT", 0),
"answer_body_logged": False,
},
"source_verification_outcome": os.environ.get("FM_COGNEE_LIVE_VERIFICATION") or "not_attempted",
"external_action_authorized": False,
}
try:
path = Path(os.environ["FM_COGNEE_LIVE_TELEMETRY_FILE"])
with path.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(event, sort_keys=True) + "\n")
except Exception:
pass
PY
) || true
}

json_payload() {
local output=$1
FM_COGNEE_QUERY=$QUERY \
FM_COGNEE_SEARCH_TYPE=${FM_COGNEE_SEARCH_TYPE:-RAG_COMPLETION} \
FM_COGNEE_TOP_K=${FM_COGNEE_TOP_K:-8} \
python3 - "$output" <<'PY'
import json
import os
import sys

try:
top_k = int(os.environ.get("FM_COGNEE_TOP_K") or 8)
except ValueError:
top_k = 8
payload = {
"query": os.environ.get("FM_COGNEE_QUERY", ""),
"searchType": os.environ.get("FM_COGNEE_SEARCH_TYPE") or "RAG_COMPLETION",
"topK": top_k,
"includeReferences": True,
}
with open(sys.argv[1], "w", encoding="utf-8") as handle:
json.dump(payload, handle)
PY
}

extract_answer_text() {
local response_file=$1 answer_file=$2 count_file=$3
python3 - "$response_file" "$answer_file" "$count_file" <<'PY'
import json
import re
import sys
from pathlib import Path


response_path, answer_path, count_path = map(Path, sys.argv[1:])
SOURCE_RE = re.compile(r"\b(?:SOURCE_ID|SOURCE_PATH|SEED_FILE)\s*[:=]")

try:
data = json.loads(response_path.read_text(encoding="utf-8"))
except Exception:
data = response_path.read_text(encoding="utf-8", errors="replace")

strings = []


def walk(value, key=""):
if isinstance(value, dict):
for child_key, child in value.items():
walk(child, str(child_key))
elif isinstance(value, list):
for child in value:
walk(child, key)
elif isinstance(value, str):
if key in {"search_result", "answer", "text", "content", "result"} or SOURCE_RE.search(value):
strings.append(value)


walk(data)
if not strings and isinstance(data, str):
strings.append(data)
answer = "\n".join(strings)
answer_path.write_text(answer, encoding="utf-8")
count_path.write_text(str(len(SOURCE_RE.findall(answer))) + "\n", encoding="utf-8")
PY
}

verification_outcome() {
local file=$1
python3 - "$file" <<'PY'
import json
import sys
from pathlib import Path

text = Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace")
lines = [line for line in text.splitlines() if line.strip()]
for line in lines:
try:
obj = json.loads(line)
except json.JSONDecodeError:
continue
result = obj.get("verification_result", {})
if result.get("outcome"):
print(result["outcome"])
raise SystemExit
for line in lines:
for token in line.split():
if token.startswith("reason="):
print(token.split("=", 1)[1])
raise SystemExit
if token.startswith("label="):
print(token.split("=", 1)[1])
raise SystemExit
print("not_attempted")
PY
}

while [ $# -gt 0 ]; do
case "$1" in
Expand Down Expand Up @@ -54,19 +273,126 @@ while [ $# -gt 0 ]; do
exit 0
;;
*)
usage
exit 1
POSITIONAL+=("$1")
shift
;;
esac
done

if [ -z "$QUERY" ] && [ "${#POSITIONAL[@]}" -gt 0 ]; then
QUERY=${POSITIONAL[*]}
fi

if ! "$DRY_RUN"; then
fm_cognee_telemetry_log \
cognee_lookup live blocked live_cognee_lookup_not_implemented 0 \
"$(fm_cognee_telemetry_latency_ms "$TELEMETRY_START_MS")" \
"" "" not_attempted "" unknown_vendor_cost "" unknown_vendor_cost
echo "label=blocked_missing_proof reason=live_cognee_lookup_not_implemented external_action_authorized=false" >&2
exit 2
[ -n "$QUERY" ] || die "--query is required in live mode"
MANIFEST=${MANIFEST:-${FM_COGNEE_MANIFEST:-}}

missing_env=
[ -n "${COGNEE_BASE_URL:-}" ] || missing_env="${missing_env:+$missing_env,}COGNEE_BASE_URL"
[ -n "${COGNEE_API_KEY:-}" ] || missing_env="${missing_env:+$missing_env,}COGNEE_API_KEY"
if [ -n "$missing_env" ]; then
live_telemetry_log blocked missing_required_env "" false 0 \
"$(fm_cognee_telemetry_latency_ms "$TELEMETRY_START_MS")" 0 missing_required_env
echo "label=blocked_missing_proof reason=missing_required_env missing_env=$missing_env external_action_authorized=false" >&2
exit 2
fi

if [ -z "$MANIFEST" ]; then
live_telemetry_log blocked missing_manifest "" false 0 \
"$(fm_cognee_telemetry_latency_ms "$TELEMETRY_START_MS")" 0 missing_manifest
echo "label=blocked_missing_proof reason=missing_manifest external_action_authorized=false" >&2
exit 2
fi
[ -f "$MANIFEST" ] || die "manifest not found: $MANIFEST"

TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/fm-cognee-live.XXXXXX")
cleanup_live() { rm -rf "$TMP_DIR"; }
trap cleanup_live EXIT
PAYLOAD="$TMP_DIR/search.json"
BODY="$TMP_DIR/body.json"
ANSWER="$TMP_DIR/answer.txt"
VERIFY_OUT="$TMP_DIR/verify.out"
COUNT_FILE="$TMP_DIR/source-count.txt"
CURL_ERR="$TMP_DIR/curl.err"
json_payload "$PAYLOAD"

base=${COGNEE_BASE_URL%/}
endpoint="$base/api/v1/search"
max_attempts=${FM_COGNEE_MAX_ATTEMPTS:-3}
case "$max_attempts" in ''|*[!0-9]*) max_attempts=3 ;; esac
[ "$max_attempts" -ge 1 ] || max_attempts=1
attempt=1
http_status=0
retryable=false
curl_rc=0
while [ "$attempt" -le "$max_attempts" ]; do
: > "$BODY"
: > "$CURL_ERR"
set +e
http_status=$(curl -sS -o "$BODY" -w '%{http_code}' \
-X POST "$endpoint" \
-H "X-Api-Key: $COGNEE_API_KEY" \
-H "Content-Type: application/json" \
--data-binary "@$PAYLOAD" 2> "$CURL_ERR")
curl_rc=$?
set -e
retryable=false
if [ "$curl_rc" -ne 0 ]; then
http_status=0
retryable=true
else
case "$http_status" in
429|500|502|503|504) retryable=true ;;
esac
fi
if ! "$retryable" || [ "$attempt" -ge "$max_attempts" ]; then
break
fi
attempt=$((attempt + 1))
done

retry_count=$((attempt - 1))
latency=$(fm_cognee_telemetry_latency_ms "$TELEMETRY_START_MS")
if [ "$curl_rc" -ne 0 ] || [ "$http_status" -lt 200 ] 2>/dev/null || [ "$http_status" -ge 300 ] 2>/dev/null; then
live_telemetry_log blocked http_or_transport_failure "$http_status" "$retryable" "$retry_count" "$latency" 0 http_or_transport_failure
echo "label=blocked_missing_proof reason=http_or_transport_failure http_status=$http_status retry_count=$retry_count retryable=$retryable external_action_authorized=false" >&2
exit 2
fi

extract_answer_text "$BODY" "$ANSWER" "$COUNT_FILE"
parsed_source_count=$(cat "$COUNT_FILE")
set +e
case "$MANIFEST" in
*.jsonl)
"$SCRIPT_DIR/fm-cognee-verify-source.sh" --manifest "$MANIFEST" --answer "$ANSWER" > "$VERIFY_OUT"
;;
*)
"$SCRIPT_DIR/fm-cognee-manifest-check.sh" --manifest "$MANIFEST" --answer-file "$ANSWER" > "$VERIFY_OUT"
;;
esac
verify_rc=$?
set -e
cat "$VERIFY_OUT"
source_outcome=$(verification_outcome "$VERIFY_OUT")
if [ "$verify_rc" -eq 0 ]; then
tel_status=verified
tel_error=none
else
tel_status=blocked
tel_error=$source_outcome
fi
live_telemetry_log "$tel_status" "$tel_error" "$http_status" false "$retry_count" "$latency" "$parsed_source_count" "$source_outcome"
echo "mode=live"
echo "dataset_alias=$(safe_label "$(dataset_alias)")"
echo "endpoint=/api/v1/search"
echo "http_status=$http_status"
echo "retry_count=$retry_count"
echo "retryable=false"
echo "parsed_source_count=$parsed_source_count"
echo "cognee_answer_status=hint_only"
echo "source_verification_outcome=$source_outcome"
echo "external_action_authorized=false"
exit "$verify_rc"
fi

[ -n "$QUERY" ] || die "--query is required in dry-run mode"
Expand Down
Loading