fix(honcho): read Honcho 3.0.x response schema — memory reads were silently empty
Nightly Build / build (push) Successful in 3m48s
Nightly Build / build (push) Successful in 3m48s
Against a self-hosted Honcho 3.0.11 every read path came back empty while the server was healthy and full of derived facts: the plugin parsed a `conclusions`/`summary` shape the API no longer emits. - honcho-client: typed models for the real schema — `PeerContext` (`representation` markdown + `peer_card`), `SessionContext` (`summary` as an object, `peer_representation`), wrapped `PeerCard` (a bare array on PUT is a 422, which also broke `honcho_profile` writes). - plugin: the turn-time injection and `honcho_context` read the representation; `honcho_search` and the page's /search now use `conclusions/query` (observer/observed scoping inside `filters`) — a real ranked semantic search with fact ids, which `peer_context?search_query` never provided; /overview returns card + representation + conclusions. - compose: pin the Honcho image by digest (3.0.11) — ghcr publishes no v3 semver tags, and an untracked `:latest` pull is what drifted the schema. - tests: fixture tests from payloads captured on the live server, plus an env-gated live smoke test (HONCHO_E2E_URL/_WS/_PEER, `cargo test -p honcho-client -- --ignored`) — run it before any future Honcho bump.
This commit is contained in:
+172
-60
@@ -70,8 +70,8 @@ use core_api::tool::{
|
||||
use core_api::user_plugin_config::PluginUserConfigApi;
|
||||
use honcho_client::HonchoClient;
|
||||
use honcho_client::models::{
|
||||
ConclusionCreate, MessageCreate, PeerCreate, PeerRepresentationGet,
|
||||
SessionCreate, SessionPeerConfig, WorkspaceCreate,
|
||||
Conclusion, ConclusionCreate, ConclusionQuery, MessageCreate, PeerContext, PeerCreate,
|
||||
PeerRepresentationGet, SessionContext, SessionCreate, SessionPeerConfig, WorkspaceCreate,
|
||||
};
|
||||
|
||||
const PLUGIN_ID: &str = "honcho";
|
||||
@@ -225,8 +225,8 @@ impl Memory for HonchoMemory {
|
||||
},
|
||||
).await {
|
||||
Ok(ctx) => {
|
||||
trace!(session_id, raw_json = %ctx, "honcho: peer_context raw response");
|
||||
let f = format_context(ctx);
|
||||
trace!(session_id, response = ?ctx, "honcho: peer_context raw response");
|
||||
let f = format_peer_context(&ctx);
|
||||
debug!(
|
||||
"honcho: peer_context (global) for session {session_id} ({} chars)",
|
||||
f.as_deref().map_or(0, |s| s.len())
|
||||
@@ -253,8 +253,8 @@ impl Memory for HonchoMemory {
|
||||
Some(user_message),
|
||||
).await {
|
||||
Ok(ctx) => {
|
||||
trace!(session_id, raw_json = %ctx, "honcho: session_context raw response");
|
||||
let f = format_context(ctx);
|
||||
trace!(session_id, response = ?ctx, "honcho: session_context raw response");
|
||||
let f = format_session_context(&ctx);
|
||||
debug!(
|
||||
"honcho: session_context for session {session_id} ({} chars)",
|
||||
f.as_deref().map_or(0, |s| s.len())
|
||||
@@ -474,19 +474,31 @@ impl Tool for HonchoProfileTool {
|
||||
gated_execution(Arc::clone(&self.user_config), ctx.user_id.clone(), move |peer| async move {
|
||||
match card_update {
|
||||
Some(facts) => {
|
||||
let facts: Vec<String> = facts
|
||||
.iter()
|
||||
.filter_map(|f| f.as_str().map(str::to_string))
|
||||
.collect();
|
||||
let n = facts.len();
|
||||
client
|
||||
.set_peer_card(&workspace_id, &peer, None, json!(facts))
|
||||
.set_peer_card(&workspace_id, &peer, None, facts)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("honcho_profile: {e}"))?;
|
||||
Ok(format!("Peer card updated ({} facts).", facts.len()))
|
||||
Ok(format!("Peer card updated ({n} facts)."))
|
||||
}
|
||||
None => {
|
||||
let card = client
|
||||
.get_peer_card(&workspace_id, &peer, None)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("honcho_profile: {e}"))?;
|
||||
Ok(serde_json::to_string_pretty(&card)
|
||||
.unwrap_or_else(|_| card.to_string()))
|
||||
// The response wraps the list: {"peer_card": [...] | null}.
|
||||
match card.peer_card.filter(|c| !c.is_empty()) {
|
||||
Some(facts) => Ok(format!(
|
||||
"Peer card ({} facts):\n- {}",
|
||||
facts.len(),
|
||||
facts.join("\n- ")
|
||||
)),
|
||||
None => Ok("No peer card set yet.".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -532,56 +544,53 @@ impl Tool for HonchoSearchTool {
|
||||
fn run_with<'a>(&'a self, ctx: &ToolContext, args: Value) -> Box<dyn ToolExecution + 'a> {
|
||||
let client = Arc::clone(&self.client);
|
||||
let workspace_id = self.workspace_id.clone();
|
||||
// Honcho's `conclusions/query` endpoint requires observer/observed
|
||||
// filters; the proven path (shared with the read-path) is `peer_context`
|
||||
// with a `search_query`, which ranks the user's conclusions by relevance.
|
||||
// `conclusions/query` is the semantic search over the user's derived
|
||||
// facts: ranked results WITH their ids (needed by `honcho_conclude`'s
|
||||
// delete). The observer/observed scoping goes inside `filters` — that
|
||||
// is the whole trick, the endpoint is the right one. (`peer_context`
|
||||
// with a search_query is not: it returns the whole representation
|
||||
// once it fits the token budget, and no ids.)
|
||||
gated_execution(Arc::clone(&self.user_config), ctx.user_id.clone(), move |peer| async move {
|
||||
let query = args["query"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("honcho_search: missing 'query' argument"))?
|
||||
.to_string();
|
||||
|
||||
let ctx = client
|
||||
.peer_context(
|
||||
let conclusions = client
|
||||
.query_conclusions(
|
||||
&workspace_id,
|
||||
&peer,
|
||||
&PeerRepresentationGet {
|
||||
search_query: Some(query),
|
||||
search_top_k: Some(10),
|
||||
..Default::default()
|
||||
&ConclusionQuery {
|
||||
query,
|
||||
top_k: Some(10),
|
||||
distance: None,
|
||||
filters: Some(json!({ "observer_id": peer, "observed_id": peer })),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("honcho_search: {e}"))?;
|
||||
|
||||
Ok(format_conclusions(&ctx)
|
||||
Ok(format_conclusions(&conclusions)
|
||||
.unwrap_or_else(|| "No relevant context found.".to_string()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats the `conclusions` array of a Honcho `peer_context` response as a
|
||||
/// ranked bullet list, prefixing each fact with its `id` when present so the
|
||||
/// model can target it via `honcho_conclude`. Returns `None` when empty.
|
||||
fn format_conclusions(ctx: &Value) -> Option<String> {
|
||||
let conclusions = ctx.get("conclusions")?.as_array()?;
|
||||
/// Formats a list of conclusions as a ranked bullet list, prefixing each fact
|
||||
/// with its `id` so the model can target it via `honcho_conclude`. Returns
|
||||
/// `None` when empty.
|
||||
fn format_conclusions(conclusions: &[Conclusion]) -> Option<String> {
|
||||
let lines: Vec<String> = conclusions
|
||||
.iter()
|
||||
.filter_map(|c| {
|
||||
let content = c.get("content").and_then(|v| v.as_str())?;
|
||||
match c.get("id").and_then(|v| v.as_str()) {
|
||||
Some(id) => Some(format!("- [{id}] {content}")),
|
||||
None => Some(format!("- {content}")),
|
||||
}
|
||||
})
|
||||
.map(|c| format!("- [{}] {}", c.id, c.content))
|
||||
.collect();
|
||||
(!lines.is_empty()).then(|| lines.join("\n"))
|
||||
}
|
||||
|
||||
// ── HonchoContextTool ─────────────────────────────────────────────────────────
|
||||
|
||||
/// Retrieves a full context snapshot for the calling user (conclusions, card,
|
||||
/// summary) from Honcho's `peer_context` endpoint. No LLM synthesis.
|
||||
/// Retrieves a full context snapshot for the calling user (the markdown
|
||||
/// representation of everything derived about them, plus their peer card)
|
||||
/// from Honcho's `peer_context` endpoint. No LLM synthesis.
|
||||
struct HonchoContextTool {
|
||||
client: Arc<HonchoClient>,
|
||||
workspace_id: String,
|
||||
@@ -626,7 +635,7 @@ impl Tool for HonchoContextTool {
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("honcho_context: {e}"))?;
|
||||
|
||||
Ok(format_context(ctx).unwrap_or_else(|| "No context available yet.".to_string()))
|
||||
Ok(format_peer_context(&ctx).unwrap_or_else(|| "No context available yet.".to_string()))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -713,36 +722,51 @@ impl Tool for HonchoConcludeTool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts a human-readable string from the raw Honcho `session_context` /
|
||||
/// `peer_context` JSON response.
|
||||
/// Formats a Honcho `peer_context` response for injection into the system
|
||||
/// prompt / as the `honcho_context` tool result.
|
||||
///
|
||||
/// Returns `None` if there is nothing *new* to inject — i.e. when the response
|
||||
/// contains only raw messages (which are already present in the LLM's own
|
||||
/// conversation history) or is otherwise empty.
|
||||
/// Honcho 3.0.x delivers everything a peer knows as a single pre-rendered
|
||||
/// markdown `representation` string (sections like `## Explicit Observations`
|
||||
/// with one dated line per fact), plus the curated `peer_card`. There is no
|
||||
/// `conclusions` array and no `summary` string in this response — parsing
|
||||
/// those keys silently yields nothing (the bug that made every read come back
|
||||
/// empty against a healthy server).
|
||||
///
|
||||
/// Only synthesised knowledge is injected:
|
||||
/// - `conclusions` — facts about the user derived by Honcho's background processing
|
||||
/// - `summary` — a narrative summary produced by Honcho
|
||||
///
|
||||
/// Raw `messages` are intentionally ignored: they are redundant with the local
|
||||
/// `chat_history` already sent to the LLM and would waste context tokens.
|
||||
fn format_context(ctx: Value) -> Option<String> {
|
||||
/// Raw `messages` are not part of this response at all; the representation is
|
||||
/// already the synthesised knowledge, so it is injected verbatim.
|
||||
fn format_peer_context(ctx: &PeerContext) -> Option<String> {
|
||||
let mut parts: Vec<String> = Vec::new();
|
||||
|
||||
if let Some(conclusions) = ctx.get("conclusions").and_then(|v| v.as_array()) {
|
||||
let facts: Vec<&str> = conclusions
|
||||
.iter()
|
||||
.filter_map(|c| c.get("content").and_then(|v| v.as_str()))
|
||||
.collect();
|
||||
if !facts.is_empty() {
|
||||
parts.push(format!("Known facts about the user:\n- {}", facts.join("\n- ")));
|
||||
}
|
||||
if let Some(card) = ctx.peer_card.as_ref().filter(|c| !c.is_empty()) {
|
||||
parts.push(format!("Peer card (curated key facts):\n- {}", card.join("\n- ")));
|
||||
}
|
||||
|
||||
if let Some(summary) = ctx.get("summary").and_then(|v| v.as_str()) {
|
||||
if !summary.trim().is_empty() {
|
||||
parts.push(format!("Conversation summary:\n{summary}"));
|
||||
}
|
||||
if let Some(rep) = ctx.representation.as_deref().map(str::trim).filter(|r| !r.is_empty()) {
|
||||
parts.push(format!("Known facts about the user:\n{rep}"));
|
||||
}
|
||||
|
||||
if parts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(format!(
|
||||
"--- Honcho memory context ---\n{}\n--- end of memory context ---",
|
||||
parts.join("\n\n")
|
||||
))
|
||||
}
|
||||
|
||||
/// Formats a Honcho `session_context` response: the running `summary` (an
|
||||
/// object — only its `content` is used) plus the session-scoped
|
||||
/// `peer_representation`. Returns `None` when neither is present.
|
||||
fn format_session_context(ctx: &SessionContext) -> Option<String> {
|
||||
let mut parts: Vec<String> = Vec::new();
|
||||
|
||||
if let Some(summary) = ctx.summary.as_ref().map(|s| s.content.trim()).filter(|s| !s.is_empty()) {
|
||||
parts.push(format!("Conversation summary:\n{summary}"));
|
||||
}
|
||||
|
||||
if let Some(rep) = ctx.peer_representation.as_deref().map(str::trim).filter(|r| !r.is_empty()) {
|
||||
parts.push(format!("Session observations:\n{rep}"));
|
||||
}
|
||||
|
||||
if parts.is_empty() {
|
||||
@@ -1147,3 +1171,91 @@ async fn get_or_create_session(
|
||||
let mut map = session_map.write().await;
|
||||
Ok(map.entry(key).or_insert(session.id).clone())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Real `peer_context` payload shape captured from a self-hosted Honcho
|
||||
/// 3.0.11: the derived facts live in the `representation` markdown string
|
||||
/// and the card is null. The pre-fix code looked for `conclusions`/
|
||||
/// `summary` keys and reported "No context available yet" against exactly
|
||||
/// this response.
|
||||
fn real_peer_context() -> PeerContext {
|
||||
serde_json::from_value(json!({
|
||||
"peer_id": "506cd15e-ae2a-47f1-9553-85ffe33e3e5b",
|
||||
"target_id": "506cd15e-ae2a-47f1-9553-85ffe33e3e5b",
|
||||
"representation": "## Explicit Observations\n\n[2026-09-09 16:20:06] 506cd15e works in an office at Battersea Power Station in London, Zone 1.\n[2026-09-09 16:55:39] Daniele ha una passione per i vulcani e ha dormito al bordo del cratere del Nyiragongo in RD Congo nel 2015.\n",
|
||||
"peer_card": null
|
||||
}))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_context_formats_the_representation() {
|
||||
let out = format_peer_context(&real_peer_context()).unwrap();
|
||||
assert!(out.contains("Nyiragongo"));
|
||||
assert!(out.contains("Battersea"));
|
||||
assert!(out.contains("--- Honcho memory context ---"));
|
||||
assert!(!out.contains("Peer card"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_context_includes_the_card_when_present() {
|
||||
let mut ctx = real_peer_context();
|
||||
ctx.peer_card = Some(vec!["Software engineer".to_string()]);
|
||||
let out = format_peer_context(&ctx).unwrap();
|
||||
assert!(out.contains("Peer card (curated key facts):\n- Software engineer"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_context_empty_means_none() {
|
||||
let ctx: PeerContext = serde_json::from_value(json!({
|
||||
"peer_id": "p", "target_id": "p", "representation": null, "peer_card": null
|
||||
}))
|
||||
.unwrap();
|
||||
assert!(format_peer_context(&ctx).is_none());
|
||||
|
||||
let ctx: PeerContext = serde_json::from_value(json!({
|
||||
"peer_id": "p", "target_id": "p", "representation": " \n ", "peer_card": []
|
||||
}))
|
||||
.unwrap();
|
||||
assert!(format_peer_context(&ctx).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_context_reads_summary_object_and_representation() {
|
||||
// Real 3.0.11 shape: `summary` is an OBJECT, not a string.
|
||||
let ctx: SessionContext = serde_json::from_value(json!({
|
||||
"id": "ws-user-1",
|
||||
"messages": [],
|
||||
"summary": {"content": "They planned a commute comparison.", "message_id": "m", "summary_type": "short", "created_at": "2026-09-09T16:00:00Z"},
|
||||
"peer_representation": "## Explicit Observations\n\n[2026-09-09] fact",
|
||||
"peer_card": null
|
||||
}))
|
||||
.unwrap();
|
||||
let out = format_session_context(&ctx).unwrap();
|
||||
assert!(out.contains("Conversation summary:\nThey planned a commute comparison."));
|
||||
assert!(out.contains("Session observations:\n## Explicit Observations"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_context_empty_means_none() {
|
||||
let ctx: SessionContext = serde_json::from_value(json!({
|
||||
"id": "s", "messages": [], "summary": null, "peer_representation": null, "peer_card": null
|
||||
}))
|
||||
.unwrap();
|
||||
assert!(format_session_context(&ctx).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conclusions_format_with_ids_for_deletion() {
|
||||
let conclusions: Vec<Conclusion> = serde_json::from_value(json!([
|
||||
{"id":"abc","content":"likes volcanoes","observer_id":"p","observed_id":"p","session_id":null,"level":"explicit","created_at":"2026-09-09T16:55:39Z"}
|
||||
]))
|
||||
.unwrap();
|
||||
let out = format_conclusions(&conclusions).unwrap();
|
||||
assert_eq!(out, "- [abc] likes volcanoes");
|
||||
assert!(format_conclusions(&[]).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
//! - `GET /status` — user-facing service health (reachability + the
|
||||
//! caller's own processing queue).
|
||||
//! - `GET /overview` — the caller's full memory snapshot (peer card +
|
||||
//! conclusions + summary). Cheap GETs, no LLM.
|
||||
//! representation digest + derived conclusions with ids). Cheap, no LLM.
|
||||
//! - `POST /search` — semantic search over the caller's derived facts.
|
||||
//! - `POST /ask` — Dialectic: Honcho's server-side LLM answers a
|
||||
//! natural-language question from the caller's memory.
|
||||
@@ -63,7 +63,10 @@ use core_api::user_channel::UserChannelApi;
|
||||
use core_api::user_plugin_config::PluginUserConfigApi;
|
||||
use honcho_client::HonchoClient;
|
||||
use honcho_client::error::HonchoError;
|
||||
use honcho_client::models::{DialecticOptions, PageParams, PeerRepresentationGet, WorkspaceGet};
|
||||
use honcho_client::models::{
|
||||
ConclusionGet, ConclusionQuery, DialecticOptions, PageParams, PeerRepresentationGet,
|
||||
WorkspaceGet,
|
||||
};
|
||||
|
||||
// Namespaced i18n keys for the router's user-facing strings (backend tables in
|
||||
// `../i18n/*.json`), resolved to the caller's language via `web.i18n`.
|
||||
@@ -334,8 +337,14 @@ async fn user_status(
|
||||
// ── GET /overview ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// The caller's full memory snapshot — the direct answer to "what does Honcho
|
||||
/// know about me?": peer card (curated key facts) + conclusions (derived facts,
|
||||
/// with their ids) + summary. Two cheap GETs, no LLM synthesis.
|
||||
/// know about me?": the peer card (curated key facts), the pre-rendered
|
||||
/// markdown `representation` (what the assistant actually receives), and the
|
||||
/// individual conclusions with their ids (for targeted deletion). Cheap, no
|
||||
/// LLM synthesis.
|
||||
///
|
||||
/// Schema note (Honcho 3.0.x): `peer_context` carries `representation` +
|
||||
/// `peer_card`, NOT a `conclusions` array — the ids come from a separate
|
||||
/// `conclusions/list` scoped to the caller's peer.
|
||||
async fn user_overview(
|
||||
State(cell): State<WebCell>,
|
||||
Extension(caller): Extension<Caller>,
|
||||
@@ -349,10 +358,12 @@ async fn user_overview(
|
||||
Err(r) => return r,
|
||||
};
|
||||
|
||||
// A 404 on either call is "no peer/card yet", not a failure — the peer is
|
||||
// A 404 on the context call is "no peer yet", not a failure — the peer is
|
||||
// created lazily by the write path on the user's first forwarded turn.
|
||||
// The card response wraps the list: `{"peer_card": [...] | null}` — unwrap
|
||||
// it so the page receives the bare value.
|
||||
let card = match web.client.get_peer_card(&web.workspace_id, &peer, None).await {
|
||||
Ok(v) => v,
|
||||
Ok(c) => json!(c.peer_card),
|
||||
Err(e @ HonchoError::Http { status: 404, .. }) => {
|
||||
debug!("honcho /overview: no card for peer '{peer}' yet: {e}");
|
||||
Value::Null
|
||||
@@ -360,7 +371,7 @@ async fn user_overview(
|
||||
Err(e) => return honcho_error(&web, &caller, &e).await,
|
||||
};
|
||||
|
||||
let ctx = match web.client.peer_context(
|
||||
let representation = match web.client.peer_context(
|
||||
&web.workspace_id,
|
||||
&peer,
|
||||
&PeerRepresentationGet {
|
||||
@@ -368,27 +379,39 @@ async fn user_overview(
|
||||
..Default::default()
|
||||
},
|
||||
).await {
|
||||
Ok(v) => v,
|
||||
Ok(ctx) => ctx.representation,
|
||||
Err(e @ HonchoError::Http { status: 404, .. }) => {
|
||||
debug!("honcho /overview: no context for peer '{peer}' yet: {e}");
|
||||
Value::Null
|
||||
None
|
||||
}
|
||||
Err(e) => return honcho_error(&web, &caller, &e).await,
|
||||
};
|
||||
|
||||
let conclusions = match web.client.list_conclusions(
|
||||
&web.workspace_id,
|
||||
&PageParams { size: Some(OVERVIEW_MAX_CONCLUSIONS as u64), ..Default::default() },
|
||||
&ConclusionGet {
|
||||
filters: Some(json!({ "observer_id": peer, "observed_id": peer })),
|
||||
},
|
||||
).await {
|
||||
Ok(page) => page.items,
|
||||
Err(e) => return honcho_error(&web, &caller, &e).await,
|
||||
};
|
||||
|
||||
Json(json!({
|
||||
"card": card,
|
||||
"conclusions": ctx.get("conclusions").cloned().unwrap_or(Value::Array(vec![])),
|
||||
"summary": ctx.get("summary").and_then(Value::as_str),
|
||||
"card": card,
|
||||
"representation": representation,
|
||||
"conclusions": conclusions,
|
||||
})).into_response()
|
||||
}
|
||||
|
||||
// ── POST /search ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// Semantic search over the caller's derived facts: `peer_context` with a
|
||||
/// `search_query`, ranked raw excerpts with their ids — no LLM synthesis. The
|
||||
/// same proven path as the `honcho_search` tool (the direct
|
||||
/// `conclusions/query` endpoint needs observer/observed filters and is not it).
|
||||
/// Semantic search over the caller's derived facts: `conclusions/query`,
|
||||
/// ranked raw excerpts with their ids — no LLM synthesis. The same path as the
|
||||
/// `honcho_search` tool; the observer/observed scoping lives in `filters`
|
||||
/// (and `peer_context` would not do: it returns the whole representation with
|
||||
/// no ids once it fits the budget).
|
||||
async fn user_search(
|
||||
State(cell): State<WebCell>,
|
||||
Extension(caller): Extension<Caller>,
|
||||
@@ -407,24 +430,16 @@ async fn user_search(
|
||||
Err(r) => return r,
|
||||
};
|
||||
|
||||
match web.client.peer_context(
|
||||
match web.client.query_conclusions(
|
||||
&web.workspace_id,
|
||||
&peer,
|
||||
&PeerRepresentationGet {
|
||||
search_query: Some(query),
|
||||
search_top_k: Some(SEARCH_TOP_K),
|
||||
..Default::default()
|
||||
&ConclusionQuery {
|
||||
query,
|
||||
top_k: Some(SEARCH_TOP_K),
|
||||
distance: None,
|
||||
filters: Some(json!({ "observer_id": peer, "observed_id": peer })),
|
||||
},
|
||||
).await {
|
||||
Ok(ctx) => Json(json!({
|
||||
"conclusions": ctx.get("conclusions").cloned().unwrap_or(Value::Array(vec![])),
|
||||
})).into_response(),
|
||||
// No peer in Honcho yet ⇒ nothing was ever derived: distinguish it from
|
||||
// "nothing matches" so the debug page can say which of the two it is.
|
||||
Err(e @ HonchoError::Http { status: 404, .. }) => {
|
||||
debug!("honcho /search: no context for peer '{peer}' yet: {e}");
|
||||
Json(json!({ "conclusions": [], "empty": true })).into_response()
|
||||
}
|
||||
Ok(conclusions) => Json(json!({ "conclusions": conclusions })).into_response(),
|
||||
Err(e) => honcho_error(&web, &caller, &e).await,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user