feat: Add presence heartbeat for Matrix online status

- matrix-gateway: POST /internal/matrix/presence/online endpoint
- usePresenceHeartbeat hook with activity tracking
- Auto away after 5 min inactivity
- Offline on page close/visibility change
- Integrated in MatrixChatRoom component
This commit is contained in:
Apple
2025-11-27 00:19:40 -08:00
parent 5bed515852
commit 3de3c8cb36
6371 changed files with 1317450 additions and 932 deletions

View File

@@ -0,0 +1,8 @@
from .http_executor import HTTPExecutor
from .python_executor import PythonExecutor
__all__ = ['HTTPExecutor', 'PythonExecutor']

View File

@@ -0,0 +1,104 @@
import httpx
import time
from typing import Dict, Any
from models import ToolCallResult
class HTTPExecutor:
"""Execute tools via HTTP calls"""
def __init__(self):
self.client = httpx.AsyncClient()
async def execute(
self,
tool_id: str,
target: str,
args: Dict[str, Any],
context: Dict[str, Any],
timeout: int = 30
) -> ToolCallResult:
"""
Execute tool via HTTP POST
Sends args + context to target URL
"""
start_time = time.time()
payload = {
"args": args,
"context": context
}
try:
response = await self.client.post(
target,
json=payload,
timeout=timeout
)
response.raise_for_status()
result = response.json()
latency_ms = (time.time() - start_time) * 1000
return ToolCallResult(
ok=True,
result=result,
tool_id=tool_id,
latency_ms=latency_ms
)
except httpx.ConnectError as e:
latency_ms = (time.time() - start_time) * 1000
error_msg = f"Connection failed: {target} ({str(e)})"
print(f"{error_msg}")
return ToolCallResult(
ok=False,
error=error_msg,
tool_id=tool_id,
latency_ms=latency_ms
)
except httpx.HTTPStatusError as e:
latency_ms = (time.time() - start_time) * 1000
error_msg = f"HTTP {e.response.status_code}: {e.response.text}"
print(f"❌ Tool {tool_id} failed: {error_msg}")
return ToolCallResult(
ok=False,
error=error_msg,
tool_id=tool_id,
latency_ms=latency_ms
)
except httpx.TimeoutException:
latency_ms = (time.time() - start_time) * 1000
error_msg = f"Timeout after {timeout}s"
print(f"⏱️ Tool {tool_id} timeout")
return ToolCallResult(
ok=False,
error=error_msg,
tool_id=tool_id,
latency_ms=latency_ms
)
except Exception as e:
latency_ms = (time.time() - start_time) * 1000
error_msg = f"Unexpected error: {str(e)}"
print(f"❌ Tool {tool_id} error: {error_msg}")
return ToolCallResult(
ok=False,
error=error_msg,
tool_id=tool_id,
latency_ms=latency_ms
)
async def close(self):
"""Close HTTP client"""
await self.client.aclose()

View File

@@ -0,0 +1,68 @@
import time
import importlib
from typing import Dict, Any
from models import ToolCallResult
class PythonExecutor:
"""
Execute tools via Python function calls
Phase 3: Stub implementation
Phase 4: Full implementation with sandboxing
"""
async def execute(
self,
tool_id: str,
target: str,
args: Dict[str, Any],
context: Dict[str, Any],
timeout: int = 30
) -> ToolCallResult:
"""
Execute Python function
target format: "module.path:function_name"
Example: "tools.projects:list_projects"
Phase 3: Stub - returns placeholder
Phase 4: Actually import and call function with proper sandboxing
"""
start_time = time.time()
print(f" Python executor (stub): {tool_id}{target}")
# TODO Phase 4: Implement proper Python execution
# 1. Parse target (module:function)
# 2. Import module dynamically
# 3. Call function with args
# 4. Add sandboxing/security
latency_ms = (time.time() - start_time) * 1000
return ToolCallResult(
ok=False,
error="Python executor not implemented in Phase 3",
tool_id=tool_id,
latency_ms=latency_ms
)
# Stub for Phase 4 implementation:
"""
async def execute_real(self, tool_id, target, args, context, timeout):
module_path, func_name = target.split(':')
module = importlib.import_module(module_path)
func = getattr(module, func_name)
# Call function with timeout
result = await asyncio.wait_for(
func(**args, context=context),
timeout=timeout
)
return ToolCallResult(ok=True, result=result, tool_id=tool_id)
"""