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.
Files changed (44) hide show
  1. roampal/__init__.py +29 -0
  2. roampal/__main__.py +6 -0
  3. roampal/backend/__init__.py +1 -0
  4. roampal/backend/modules/__init__.py +1 -0
  5. roampal/backend/modules/memory/__init__.py +43 -0
  6. roampal/backend/modules/memory/chromadb_adapter.py +623 -0
  7. roampal/backend/modules/memory/config.py +102 -0
  8. roampal/backend/modules/memory/content_graph.py +543 -0
  9. roampal/backend/modules/memory/context_service.py +455 -0
  10. roampal/backend/modules/memory/embedding_service.py +96 -0
  11. roampal/backend/modules/memory/knowledge_graph_service.py +1052 -0
  12. roampal/backend/modules/memory/memory_bank_service.py +433 -0
  13. roampal/backend/modules/memory/memory_types.py +296 -0
  14. roampal/backend/modules/memory/outcome_service.py +400 -0
  15. roampal/backend/modules/memory/promotion_service.py +473 -0
  16. roampal/backend/modules/memory/routing_service.py +444 -0
  17. roampal/backend/modules/memory/scoring_service.py +324 -0
  18. roampal/backend/modules/memory/search_service.py +646 -0
  19. roampal/backend/modules/memory/tests/__init__.py +1 -0
  20. roampal/backend/modules/memory/tests/conftest.py +12 -0
  21. roampal/backend/modules/memory/tests/unit/__init__.py +1 -0
  22. roampal/backend/modules/memory/tests/unit/conftest.py +7 -0
  23. roampal/backend/modules/memory/tests/unit/test_knowledge_graph_service.py +517 -0
  24. roampal/backend/modules/memory/tests/unit/test_memory_bank_service.py +504 -0
  25. roampal/backend/modules/memory/tests/unit/test_outcome_service.py +485 -0
  26. roampal/backend/modules/memory/tests/unit/test_scoring_service.py +255 -0
  27. roampal/backend/modules/memory/tests/unit/test_search_service.py +413 -0
  28. roampal/backend/modules/memory/tests/unit/test_unified_memory_system.py +418 -0
  29. roampal/backend/modules/memory/unified_memory_system.py +1277 -0
  30. roampal/cli.py +638 -0
  31. roampal/hooks/__init__.py +16 -0
  32. roampal/hooks/session_manager.py +587 -0
  33. roampal/hooks/stop_hook.py +176 -0
  34. roampal/hooks/user_prompt_submit_hook.py +103 -0
  35. roampal/mcp/__init__.py +7 -0
  36. roampal/mcp/server.py +611 -0
  37. roampal/server/__init__.py +7 -0
  38. roampal/server/main.py +744 -0
  39. roampal-0.1.4.dist-info/METADATA +179 -0
  40. roampal-0.1.4.dist-info/RECORD +44 -0
  41. roampal-0.1.4.dist-info/WHEEL +5 -0
  42. roampal-0.1.4.dist-info/entry_points.txt +2 -0
  43. roampal-0.1.4.dist-info/licenses/LICENSE +190 -0
  44. 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()
@@ -0,0 +1,7 @@
1
+ """
2
+ Roampal MCP Server - Model Context Protocol tools
3
+ """
4
+
5
+ from .server import run_mcp_server
6
+
7
+ __all__ = ["run_mcp_server"]