Skip to content

Commit 30af066

Browse files
authored
Feat/node xtrace memory (#1065)
* feat(node): add xTrace Memory tool node (#1038) Long-term, shared agent memory exposed as tools, backed by xTrace Memory Manager's HTTP API. 'remember' ingests conversation turns (POST /v1/memories, sync wait + poll) and 'recall' searches the pool (POST /v1/memories/search, compose mode returns ready-to-inject context). classType tool, so it complements the agent's required short-term memory subsystem. Includes example pipe and unit tests (helpers, payloads, scope/validation). * fix(node): xtrace_memory — drop experimental flag, make non-essential fields optional Only api_key and org_id are required now; base_url, user_id, agent_id, app_id, group_ids, wait, ingest_timeout, extract_artifacts, search_mode and search_limit are marked optional (they have sensible defaults), so the config panel no longer shows them as required. Removes the 'experimental' capability so the node card no longer shows the EXPERIMENTAL badge. * fix(node): xtrace_memory — minimal hackathon config (API key + org id) Trim the config panel to the essentials: API Key and Org id (required), plus optional User id and Group ids (shared memory). Hide the advanced knobs (base_url, agent_id, app_id, synchronous ingest, ingest timeout, extract_artifacts, recall mode/limit) — they keep sensible defaults in IGlobal, so behavior is unchanged but the UI is much simpler. * feat(node): xtrace_memory — move advanced options behind an 'Advanced settings' toggle Keep the panel minimal by default (API Key, Org id, User id, Group ids) and reveal the advanced knobs (base_url, agent_id, app_id, synchronous ingest, ingest timeout, extract_artifacts, recall mode/limit) only when the user flips the 'Advanced settings' toggle — same conditional pattern llamaparse and tool_mcp_client use. Defaults unchanged. * fix(example): xtrace-memory pipe — use ROCKETRIDE_-prefixed env vars RocketRide only substitutes ${ROCKETRIDE_*} placeholders in pipelines, so ${XTRACE_API_KEY}/${XTRACE_ORG_ID} were never resolved. Use ${ROCKETRIDE_XTRACE_API_KEY} and ${ROCKETRIDE_XTRACE_ORG_ID}. * docs(node): xtrace_memory — point API Key field/README to app.xtrace.ai (not mem.xtrace.ai) Surface where to get credentials right in the form: the API Key and Org id field help now link to the Developer Portal (app.xtrace.ai → Settings → API Keys) and warn that mem.xtrace.ai (memhub/MCP, OAuth) has no API key. README gets a matching 'Where to get your credentials' section. * docs(node): xtrace_memory — drop MCP/OAuth mention from credential help Keep the API Key/Org id guidance focused: get them from app.xtrace.ai → Settings → API Keys (not mem.xtrace.ai). The MCP server / OAuth path isn't relevant to this node, so it's removed from the field help and README. * Working example * feat(node): xtrace_memory — use the real xTrace logo as the node icon * feat(node): xtrace_memory — use official xTrace vector logo as the icon Replace the embedded-PNG placeholder with the official XTraceAI SVG (vector mark + wordmark, animated gradient). * feat(node): xtrace_memory — icon = xTrace mark only (drop wordmark) The node header already shows the name, so the icon now uses just the xTrace symbol (viewBox 0 0 140 140) instead of the full XTraceAI wordmark, which was illegible squished into the small square icon slot. * fix(node): xtrace_memory — don't retry non-idempotent ingest on 5xx/timeout _request_with_retry gains an 'idempotent' flag (default True). The ingest POST passes idempotent=False so it only retries on 429 (not processed), avoiding duplicate memories on ambiguous 5xx/timeout. Reads (search, poll) keep full retries. Addresses CodeRabbit review on PR #1065. * docs(node): xtrace_memory — add explanatory comments to services.json (match llm_anthropic style) * refactor(node): xtrace_memory — address review (tenacity, constants, docstring, skip dynamic test) - Retries via tenacity (Retrying + retry_if_exception) instead of a hand-rolled loop, keeping the idempotent gating (429 always; 5xx/timeout only for reads). - Extract magic constants: _DEFAULT_BASE_URL, _DEFAULT_INGEST_TIMEOUT, _MAX_INGEST_TIMEOUT, _REQUEST_TIMEOUT. - Full Args/Returns/Raises docstring on _request_with_retry. - Add tool_xtrace_memory to conftest skip_nodes: its dynamic test needs live xTrace credentials (no live calls in default CI; opt-in via ROCKETRIDE_INCLUDE_SKIP). Mocked unit tests still cover the logic.
1 parent b36137e commit 30af066

11 files changed

Lines changed: 1321 additions & 0 deletions

File tree

examples/xtrace-memory-agent.pipe

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
{
2+
"components": [
3+
{
4+
"id": "chat_1",
5+
"provider": "chat",
6+
"name": "Chat",
7+
"config": {
8+
"hideForm": true,
9+
"mode": "Source",
10+
"parameters": {},
11+
"type": "chat"
12+
},
13+
"ui": {
14+
"position": {
15+
"x": 20,
16+
"y": 200
17+
},
18+
"nodeType": "default",
19+
"formDataValid": true
20+
}
21+
},
22+
{
23+
"id": "agent_rocketride_1",
24+
"provider": "agent_rocketride",
25+
"name": "RocketRide Wave",
26+
"config": {
27+
"instructions": [
28+
"You are a personal assistant with long-term, shared memory via the xtrace tools.",
29+
"At the start of each turn, call xtrace.recall with a query describing what you need to know about the user, and use the returned context.",
30+
"After answering, call xtrace.remember with the relevant turns so you (or other agents sharing this memory) can use them later.",
31+
"Use your internal memory only for short-term working notes within this run."
32+
],
33+
"max_waves": 15,
34+
"parameters": {}
35+
},
36+
"ui": {
37+
"position": {
38+
"x": 240,
39+
"y": 200
40+
},
41+
"nodeType": "default",
42+
"formDataValid": true
43+
},
44+
"input": [
45+
{
46+
"lane": "questions",
47+
"from": "chat_1"
48+
}
49+
]
50+
},
51+
{
52+
"id": "memory_internal_1",
53+
"provider": "memory_internal",
54+
"name": "Memory (Internal)",
55+
"config": {
56+
"type": "memory_internal"
57+
},
58+
"ui": {
59+
"position": {
60+
"x": 300,
61+
"y": 360
62+
},
63+
"nodeType": "default",
64+
"formDataValid": true
65+
},
66+
"control": [
67+
{
68+
"classType": "memory",
69+
"from": "agent_rocketride_1"
70+
}
71+
]
72+
},
73+
{
74+
"id": "tool_xtrace_memory_1",
75+
"provider": "tool_xtrace_memory",
76+
"name": "xTrace Memory",
77+
"config": {
78+
"api_key": "${ROCKETRIDE_XTRACE_API_KEY}",
79+
"group_ids": "",
80+
"org_id": "${ROCKETRIDE_XTRACE_ORG_ID}",
81+
"show_advanced": false,
82+
"type": "tool_xtrace_memory",
83+
"user_id": "demo-user",
84+
"agent_id": "assistant",
85+
"search_mode": "compose",
86+
"parameters": {
87+
"google": {}
88+
}
89+
},
90+
"ui": {
91+
"position": {
92+
"x": 500,
93+
"y": 360
94+
},
95+
"nodeType": "default",
96+
"formDataValid": true
97+
},
98+
"control": [
99+
{
100+
"classType": "tool",
101+
"from": "agent_rocketride_1"
102+
}
103+
]
104+
},
105+
{
106+
"id": "response_answers_1",
107+
"provider": "response_answers",
108+
"name": "Return Answers",
109+
"config": {
110+
"laneName": "answers"
111+
},
112+
"ui": {
113+
"position": {
114+
"x": 460,
115+
"y": 200
116+
},
117+
"nodeType": "default",
118+
"formDataValid": true
119+
},
120+
"input": [
121+
{
122+
"lane": "answers",
123+
"from": "agent_rocketride_1"
124+
}
125+
]
126+
},
127+
{
128+
"id": "llm_anthropic_1",
129+
"provider": "llm_anthropic",
130+
"name": "Anthropic",
131+
"config": {
132+
"profile": "claude-sonnet-4-6",
133+
"claude-sonnet-4-6": {
134+
"apikey": "${ROCKETRIDE_ANTHROPIC_KEY}"
135+
},
136+
"name": "Anthropic",
137+
"parameters": {
138+
"google": {}
139+
}
140+
},
141+
"ui": {
142+
"position": {
143+
"x": 100,
144+
"y": 360
145+
},
146+
"nodeType": "default",
147+
"formDataValid": true
148+
},
149+
"control": [
150+
{
151+
"classType": "llm",
152+
"from": "agent_rocketride_1"
153+
}
154+
]
155+
}
156+
],
157+
"project_id": "5ee7af1b-5b4b-479f-b10a-18ccc83d3136",
158+
"version": 1,
159+
"isLocked": false,
160+
"snapToGrid": true,
161+
"snapGridSize": [
162+
10,
163+
10
164+
],
165+
"docRevision": 13
166+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# =============================================================================
2+
# MIT License
3+
# Copyright (c) 2026 Aparavi Software AG
4+
# =============================================================================
5+
6+
"""
7+
xTrace Memory node — global (per-pipe) state.
8+
9+
Reads the xTrace credentials and default scope from the node config and
10+
holds them for the instance-level tool functions. Connection details are
11+
validated once here so every ``remember`` / ``recall`` call reuses them.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import os
17+
from typing import List
18+
19+
from ai.common.config import Config
20+
from rocketlib import IGlobalBase, OPEN_MODE, error, warning
21+
22+
23+
def _split_group_ids(raw: object) -> List[str]:
24+
"""Accept a comma-separated string or a list and return a clean id list."""
25+
if isinstance(raw, list):
26+
return [str(g).strip() for g in raw if str(g).strip()]
27+
if isinstance(raw, str):
28+
return [g.strip() for g in raw.split(',') if g.strip()]
29+
return []
30+
31+
32+
# Defaults / bounds (avoid magic constants scattered in the code).
33+
_DEFAULT_BASE_URL = 'https://api.production.xtrace.ai'
34+
_DEFAULT_INGEST_TIMEOUT = 30
35+
_MAX_INGEST_TIMEOUT = 120
36+
37+
38+
class IGlobal(IGlobalBase):
39+
"""Global state for xtrace_memory."""
40+
41+
api_key: str = ''
42+
org_id: str = ''
43+
base_url: str = _DEFAULT_BASE_URL
44+
user_id: str = ''
45+
agent_id: str = ''
46+
app_id: str = ''
47+
group_ids: List[str] = []
48+
wait: bool = True
49+
ingest_timeout: int = _DEFAULT_INGEST_TIMEOUT
50+
extract_artifacts: bool = False
51+
search_mode: str = 'compose'
52+
search_limit: int = 10
53+
54+
def beginGlobal(self) -> None:
55+
if self.IEndpoint.endpoint.openMode == OPEN_MODE.CONFIG:
56+
return
57+
58+
cfg = Config.getNodeConfig(self.glb.logicalType, self.glb.connConfig)
59+
60+
api_key = str(cfg.get('api_key') or os.environ.get('XTRACE_API_KEY', '')).strip()
61+
org_id = str(cfg.get('org_id') or os.environ.get('XTRACE_ORG_ID', '')).strip()
62+
63+
if not api_key:
64+
error('xtrace_memory: api_key is required — set it in node config or the XTRACE_API_KEY env var')
65+
raise ValueError('xtrace_memory: api_key is required')
66+
if not org_id:
67+
error('xtrace_memory: org_id is required — set it in node config or the XTRACE_ORG_ID env var')
68+
raise ValueError('xtrace_memory: org_id is required')
69+
70+
self.api_key = api_key
71+
self.org_id = org_id
72+
self.base_url = str(cfg.get('base_url') or _DEFAULT_BASE_URL).strip().rstrip('/')
73+
self.user_id = str(cfg.get('user_id') or '').strip()
74+
self.agent_id = str(cfg.get('agent_id') or '').strip()
75+
self.app_id = str(cfg.get('app_id') or '').strip()
76+
self.group_ids = _split_group_ids(cfg.get('group_ids'))
77+
self.wait = bool(cfg.get('wait', True))
78+
79+
raw_timeout = cfg.get('ingest_timeout', _DEFAULT_INGEST_TIMEOUT)
80+
self.ingest_timeout = max(
81+
1, min(_MAX_INGEST_TIMEOUT, int(raw_timeout if raw_timeout is not None else _DEFAULT_INGEST_TIMEOUT))
82+
)
83+
84+
self.extract_artifacts = bool(cfg.get('extract_artifacts', False))
85+
86+
mode = str(cfg.get('search_mode') or 'compose').strip().lower()
87+
self.search_mode = mode if mode in ('compose', 'retrieve') else 'compose'
88+
89+
raw_limit = cfg.get('search_limit', 10)
90+
self.search_limit = max(1, min(100, int(raw_limit if raw_limit is not None else 10)))
91+
92+
def validateConfig(self) -> None:
93+
try:
94+
cfg = Config.getNodeConfig(self.glb.logicalType, self.glb.connConfig)
95+
api_key = str(cfg.get('api_key') or os.environ.get('XTRACE_API_KEY', '')).strip()
96+
org_id = str(cfg.get('org_id') or os.environ.get('XTRACE_ORG_ID', '')).strip()
97+
if not api_key:
98+
warning('api_key is required')
99+
if not org_id:
100+
warning('org_id is required')
101+
except Exception as e:
102+
warning(str(e))
103+
104+
def endGlobal(self) -> None:
105+
self.api_key = ''

0 commit comments

Comments
 (0)