roampal 0.1.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- roampal/__init__.py +29 -0
- roampal/__main__.py +6 -0
- roampal/backend/__init__.py +1 -0
- roampal/backend/modules/__init__.py +1 -0
- roampal/backend/modules/memory/__init__.py +43 -0
- roampal/backend/modules/memory/chromadb_adapter.py +623 -0
- roampal/backend/modules/memory/config.py +102 -0
- roampal/backend/modules/memory/content_graph.py +543 -0
- roampal/backend/modules/memory/context_service.py +455 -0
- roampal/backend/modules/memory/embedding_service.py +96 -0
- roampal/backend/modules/memory/knowledge_graph_service.py +1052 -0
- roampal/backend/modules/memory/memory_bank_service.py +433 -0
- roampal/backend/modules/memory/memory_types.py +296 -0
- roampal/backend/modules/memory/outcome_service.py +400 -0
- roampal/backend/modules/memory/promotion_service.py +473 -0
- roampal/backend/modules/memory/routing_service.py +444 -0
- roampal/backend/modules/memory/scoring_service.py +324 -0
- roampal/backend/modules/memory/search_service.py +646 -0
- roampal/backend/modules/memory/tests/__init__.py +1 -0
- roampal/backend/modules/memory/tests/conftest.py +12 -0
- roampal/backend/modules/memory/tests/unit/__init__.py +1 -0
- roampal/backend/modules/memory/tests/unit/conftest.py +7 -0
- roampal/backend/modules/memory/tests/unit/test_knowledge_graph_service.py +517 -0
- roampal/backend/modules/memory/tests/unit/test_memory_bank_service.py +504 -0
- roampal/backend/modules/memory/tests/unit/test_outcome_service.py +485 -0
- roampal/backend/modules/memory/tests/unit/test_scoring_service.py +255 -0
- roampal/backend/modules/memory/tests/unit/test_search_service.py +413 -0
- roampal/backend/modules/memory/tests/unit/test_unified_memory_system.py +418 -0
- roampal/backend/modules/memory/unified_memory_system.py +1277 -0
- roampal/cli.py +638 -0
- roampal/hooks/__init__.py +16 -0
- roampal/hooks/session_manager.py +587 -0
- roampal/hooks/stop_hook.py +176 -0
- roampal/hooks/user_prompt_submit_hook.py +103 -0
- roampal/mcp/__init__.py +7 -0
- roampal/mcp/server.py +611 -0
- roampal/server/__init__.py +7 -0
- roampal/server/main.py +744 -0
- roampal-0.1.4.dist-info/METADATA +179 -0
- roampal-0.1.4.dist-info/RECORD +44 -0
- roampal-0.1.4.dist-info/WHEEL +5 -0
- roampal-0.1.4.dist-info/entry_points.txt +2 -0
- roampal-0.1.4.dist-info/licenses/LICENSE +190 -0
- roampal-0.1.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Roampal Stop Hook
|
|
4
|
+
|
|
5
|
+
Called by Claude Code AFTER the LLM responds.
|
|
6
|
+
This hook:
|
|
7
|
+
1. Stores the exchange for later scoring
|
|
8
|
+
2. Checks if record_response() was called
|
|
9
|
+
3. BLOCKS (exit 2) if scoring was required but not done
|
|
10
|
+
|
|
11
|
+
Usage (in .claude/settings.json):
|
|
12
|
+
{
|
|
13
|
+
"hooks": {
|
|
14
|
+
"Stop": [{"type": "command", "command": "python -m roampal.hooks.stop_hook"}]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Environment variables:
|
|
19
|
+
- ROAMPAL_SERVER_URL: URL of Roampal server (default: http://127.0.0.1:27182)
|
|
20
|
+
|
|
21
|
+
Reads from stdin (Claude Code format):
|
|
22
|
+
- session_id: Conversation session ID
|
|
23
|
+
- transcript_path: Path to JSONL file with conversation history
|
|
24
|
+
- stop_hook_active: Boolean to prevent infinite loops
|
|
25
|
+
|
|
26
|
+
Exit codes:
|
|
27
|
+
- 0: Success, continue
|
|
28
|
+
- 2: Block - record_response() not called, inject message back to LLM
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import sys
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
import urllib.request
|
|
35
|
+
import urllib.error
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def read_transcript(transcript_path: str) -> tuple[str, str, str]:
|
|
39
|
+
"""
|
|
40
|
+
Read the transcript JSONL file and extract last user message,
|
|
41
|
+
assistant response, and full transcript text.
|
|
42
|
+
|
|
43
|
+
Claude Code transcript format:
|
|
44
|
+
- type: "user" or "assistant" (top level)
|
|
45
|
+
- message: { role: "user"|"assistant", content: [...] }
|
|
46
|
+
"""
|
|
47
|
+
user_message = ""
|
|
48
|
+
assistant_response = ""
|
|
49
|
+
transcript_lines = []
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
with open(transcript_path, 'r', encoding='utf-8') as f:
|
|
53
|
+
for line in f:
|
|
54
|
+
line = line.strip()
|
|
55
|
+
if not line:
|
|
56
|
+
continue
|
|
57
|
+
try:
|
|
58
|
+
entry = json.loads(line)
|
|
59
|
+
|
|
60
|
+
# Claude Code uses "type" at top level, not "role"
|
|
61
|
+
entry_type = entry.get("type", "")
|
|
62
|
+
|
|
63
|
+
# Content can be in message.content or directly in entry
|
|
64
|
+
message = entry.get("message", {})
|
|
65
|
+
if isinstance(message, dict):
|
|
66
|
+
content = message.get("content", "")
|
|
67
|
+
else:
|
|
68
|
+
content = entry.get("content", "")
|
|
69
|
+
|
|
70
|
+
# Handle content that might be a list of content blocks
|
|
71
|
+
if isinstance(content, list):
|
|
72
|
+
text_parts = []
|
|
73
|
+
for block in content:
|
|
74
|
+
if isinstance(block, dict):
|
|
75
|
+
if block.get("type") == "text":
|
|
76
|
+
text_parts.append(block.get("text", ""))
|
|
77
|
+
elif block.get("type") == "tool_use":
|
|
78
|
+
# Include tool calls in transcript for record_response detection
|
|
79
|
+
tool_name = block.get("name", "")
|
|
80
|
+
text_parts.append(f"[Tool: {tool_name}]")
|
|
81
|
+
elif isinstance(block, str):
|
|
82
|
+
text_parts.append(block)
|
|
83
|
+
content = "\n".join(text_parts)
|
|
84
|
+
|
|
85
|
+
if entry_type == "user":
|
|
86
|
+
user_message = content if content else user_message
|
|
87
|
+
if content:
|
|
88
|
+
transcript_lines.append(f"User: {content}")
|
|
89
|
+
elif entry_type == "assistant":
|
|
90
|
+
assistant_response = content if content else assistant_response
|
|
91
|
+
if content:
|
|
92
|
+
transcript_lines.append(f"Assistant: {content}")
|
|
93
|
+
except json.JSONDecodeError:
|
|
94
|
+
continue
|
|
95
|
+
except Exception as e:
|
|
96
|
+
print(f"Error reading transcript: {e}", file=sys.stderr)
|
|
97
|
+
|
|
98
|
+
return user_message, assistant_response, "\n\n".join(transcript_lines)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def main():
|
|
102
|
+
# Read hook input from stdin
|
|
103
|
+
try:
|
|
104
|
+
input_data = json.load(sys.stdin)
|
|
105
|
+
except json.JSONDecodeError:
|
|
106
|
+
# No input or invalid JSON - just exit cleanly
|
|
107
|
+
sys.exit(0)
|
|
108
|
+
|
|
109
|
+
# Check if this is already a stop hook continuation (prevent infinite loops)
|
|
110
|
+
if input_data.get("stop_hook_active", False):
|
|
111
|
+
sys.exit(0)
|
|
112
|
+
|
|
113
|
+
# Extract conversation data - Claude Code sends transcript_path, not raw content
|
|
114
|
+
conversation_id = input_data.get("session_id", os.environ.get("ROAMPAL_CONVERSATION_ID", "default"))
|
|
115
|
+
transcript_path = input_data.get("transcript_path", "")
|
|
116
|
+
|
|
117
|
+
# Read the transcript file to get actual messages
|
|
118
|
+
if transcript_path and os.path.exists(transcript_path):
|
|
119
|
+
user_message, assistant_response, transcript = read_transcript(transcript_path)
|
|
120
|
+
else:
|
|
121
|
+
# Fallback for direct input (testing)
|
|
122
|
+
user_message = input_data.get("user_message", "")
|
|
123
|
+
assistant_response = input_data.get("assistant_response", "")
|
|
124
|
+
transcript = input_data.get("transcript", "")
|
|
125
|
+
|
|
126
|
+
# If no messages, nothing to do
|
|
127
|
+
if not user_message and not assistant_response:
|
|
128
|
+
print(f"Stop hook: no messages found for {conversation_id}, transcript_path={transcript_path}", file=sys.stderr)
|
|
129
|
+
sys.exit(0)
|
|
130
|
+
|
|
131
|
+
# Call Roampal server
|
|
132
|
+
server_url = os.environ.get("ROAMPAL_SERVER_URL", "http://127.0.0.1:27182")
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
request_data = json.dumps({
|
|
136
|
+
"conversation_id": conversation_id,
|
|
137
|
+
"user_message": user_message,
|
|
138
|
+
"assistant_response": assistant_response,
|
|
139
|
+
"transcript": transcript
|
|
140
|
+
}).encode("utf-8")
|
|
141
|
+
|
|
142
|
+
req = urllib.request.Request(
|
|
143
|
+
f"{server_url}/api/hooks/stop",
|
|
144
|
+
data=request_data,
|
|
145
|
+
headers={"Content-Type": "application/json"},
|
|
146
|
+
method="POST"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
with urllib.request.urlopen(req, timeout=5) as response:
|
|
150
|
+
result = json.loads(response.read().decode("utf-8"))
|
|
151
|
+
|
|
152
|
+
# Check if we should block
|
|
153
|
+
if result.get("should_block"):
|
|
154
|
+
# Output the block message to stderr - exit code 2 shows stderr to Claude
|
|
155
|
+
block_message = result.get("block_message", "")
|
|
156
|
+
if block_message:
|
|
157
|
+
print(block_message, file=sys.stderr)
|
|
158
|
+
|
|
159
|
+
# Exit code 2 = block stopping, shows stderr to Claude
|
|
160
|
+
sys.exit(2)
|
|
161
|
+
|
|
162
|
+
# Success - exchange stored
|
|
163
|
+
sys.exit(0)
|
|
164
|
+
|
|
165
|
+
except urllib.error.URLError as e:
|
|
166
|
+
# Server not running - log but don't block
|
|
167
|
+
print(f"Roampal server unavailable: {e}", file=sys.stderr)
|
|
168
|
+
sys.exit(0)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
# Other error - log but don't block
|
|
171
|
+
print(f"Roampal hook error: {e}", file=sys.stderr)
|
|
172
|
+
sys.exit(0)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
main()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Roampal UserPromptSubmit Hook
|
|
4
|
+
|
|
5
|
+
Called by Claude Code BEFORE the LLM sees the user's message.
|
|
6
|
+
This hook:
|
|
7
|
+
1. Checks if previous exchange needs scoring
|
|
8
|
+
2. Injects scoring prompt if needed
|
|
9
|
+
3. Injects relevant memories as context
|
|
10
|
+
|
|
11
|
+
Usage (in .claude/settings.json):
|
|
12
|
+
{
|
|
13
|
+
"hooks": {
|
|
14
|
+
"UserPromptSubmit": ["python", "-m", "roampal.hooks.user_prompt_submit_hook"]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Environment variables:
|
|
19
|
+
- ROAMPAL_SERVER_URL: URL of Roampal server (default: http://127.0.0.1:27182)
|
|
20
|
+
|
|
21
|
+
Reads from stdin:
|
|
22
|
+
- JSON with user_message
|
|
23
|
+
|
|
24
|
+
Outputs to stdout:
|
|
25
|
+
- Modified user message with injected context (prepended)
|
|
26
|
+
|
|
27
|
+
Exit codes:
|
|
28
|
+
- 0: Success
|
|
29
|
+
- 1: Error (but don't break the flow)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import sys
|
|
33
|
+
import json
|
|
34
|
+
import os
|
|
35
|
+
import urllib.request
|
|
36
|
+
import urllib.error
|
|
37
|
+
|
|
38
|
+
# Fix Windows encoding issues with unicode characters
|
|
39
|
+
if sys.platform == "win32":
|
|
40
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
41
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main():
|
|
45
|
+
# Read hook input from stdin
|
|
46
|
+
try:
|
|
47
|
+
input_data = json.load(sys.stdin)
|
|
48
|
+
except json.JSONDecodeError:
|
|
49
|
+
# No input - pass through
|
|
50
|
+
sys.exit(0)
|
|
51
|
+
|
|
52
|
+
# Claude Code sends "prompt" field
|
|
53
|
+
user_message = input_data.get("prompt", input_data.get("user_message", input_data.get("query", "")))
|
|
54
|
+
|
|
55
|
+
if not user_message:
|
|
56
|
+
sys.exit(0)
|
|
57
|
+
|
|
58
|
+
# Get session_id from Claude Code input - this matches what Stop hook uses
|
|
59
|
+
# This ensures completion state is tracked consistently across hooks
|
|
60
|
+
conversation_id = input_data.get("session_id", "default")
|
|
61
|
+
|
|
62
|
+
# Call Roampal server for context
|
|
63
|
+
server_url = os.environ.get("ROAMPAL_SERVER_URL", "http://127.0.0.1:27182")
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
request_data = json.dumps({
|
|
67
|
+
"query": user_message,
|
|
68
|
+
"conversation_id": conversation_id
|
|
69
|
+
}).encode("utf-8")
|
|
70
|
+
|
|
71
|
+
req = urllib.request.Request(
|
|
72
|
+
f"{server_url}/api/hooks/get-context",
|
|
73
|
+
data=request_data,
|
|
74
|
+
headers={"Content-Type": "application/json"},
|
|
75
|
+
method="POST"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
with urllib.request.urlopen(req, timeout=5) as response:
|
|
79
|
+
result = json.loads(response.read().decode("utf-8"))
|
|
80
|
+
|
|
81
|
+
# Get the formatted injection
|
|
82
|
+
formatted_injection = result.get("formatted_injection", "")
|
|
83
|
+
|
|
84
|
+
if formatted_injection:
|
|
85
|
+
# Print context to stdout - Claude Code adds this to conversation
|
|
86
|
+
print(formatted_injection)
|
|
87
|
+
|
|
88
|
+
# Exit 0 = success, stdout added as context
|
|
89
|
+
|
|
90
|
+
sys.exit(0)
|
|
91
|
+
|
|
92
|
+
except urllib.error.URLError as e:
|
|
93
|
+
# Server not running - no context to inject
|
|
94
|
+
print(f"Roampal server unavailable: {e}", file=sys.stderr)
|
|
95
|
+
sys.exit(0)
|
|
96
|
+
except Exception as e:
|
|
97
|
+
# Other error - no context to inject
|
|
98
|
+
print(f"Roampal hook error: {e}", file=sys.stderr)
|
|
99
|
+
sys.exit(0)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
main()
|