Compare commits
11 Commits
fix/kimi-f
...
feat/mempa
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a6045a76a | |||
| 4ef7b5fc46 | |||
| 7d2421a15f | |||
|
|
5a942d71a1 | ||
|
|
044f0f8951 | ||
| 61c59ce332 | |||
| 01ce8ae889 | |||
|
|
b179250ab8 | ||
| 01a3f47a5b | |||
|
|
4538e11f97 | ||
| 7936483ffc |
@@ -47,6 +47,11 @@ jobs:
|
||||
source .venv/bin/activate
|
||||
python scripts/syntax_guard.py
|
||||
|
||||
- name: No duplicate models
|
||||
run: |
|
||||
source .venv/bin/activate
|
||||
python scripts/check_no_duplicate_models.py
|
||||
|
||||
- name: Green-path E2E
|
||||
run: |
|
||||
source .venv/bin/activate
|
||||
|
||||
@@ -940,7 +940,7 @@ def _try_ollama() -> Tuple[Optional[OpenAI], Optional[str]]:
|
||||
return None, None
|
||||
api_key = (os.getenv("OLLAMA_API_KEY", "") or "ollama").strip()
|
||||
model = _read_main_model() or "gemma4:12b"
|
||||
return OpenAI(api_key=*** base_url=base_url), model
|
||||
return OpenAI(api_key=api_key, base_url=base_url), model
|
||||
|
||||
|
||||
def _get_provider_chain() -> List[tuple]:
|
||||
@@ -1216,7 +1216,7 @@ def resolve_provider_client(
|
||||
base_url = base_url + "/v1" if not base_url.endswith("/v1") else base_url
|
||||
api_key = (explicit_api_key or os.getenv("OLLAMA_API_KEY", "") or "ollama").strip()
|
||||
final_model = model or _read_main_model() or "gemma4:12b"
|
||||
client = OpenAI(api_key=*** base_url=base_url)
|
||||
client = OpenAI(api_key=api_key, base_url=base_url)
|
||||
return (_to_async_client(client, final_model) if async_mode else (client, final_model))
|
||||
|
||||
# ── Custom endpoint (OPENAI_BASE_URL + OPENAI_API_KEY) ───────────
|
||||
|
||||
@@ -148,7 +148,7 @@ PROVIDER_TO_MODELS_DEV: Dict[str, str] = {
|
||||
"openrouter": "openrouter",
|
||||
"anthropic": "anthropic",
|
||||
"zai": "zai",
|
||||
"kimi-coding": "kimi-for-coding",
|
||||
"kimi-coding": "kimi-k2.5",
|
||||
"minimax": "minimax",
|
||||
"minimax-cn": "minimax-cn",
|
||||
"deepseek": "deepseek",
|
||||
|
||||
@@ -2126,7 +2126,7 @@ def _model_flow_kimi(config, current_model=""):
|
||||
|
||||
# Step 3: Model selection — show appropriate models for the endpoint
|
||||
if is_coding_plan:
|
||||
# Coding Plan models (kimi-k2.5 first — kimi-for-coding retired due to 403)
|
||||
# Coding Plan models (kimi-k2.5 first)
|
||||
model_list = [
|
||||
"kimi-k2.5",
|
||||
"kimi-k2-thinking",
|
||||
|
||||
@@ -78,7 +78,7 @@ HERMES_OVERLAYS: Dict[str, HermesOverlay] = {
|
||||
extra_env_vars=("GLM_API_KEY", "ZAI_API_KEY", "Z_AI_API_KEY"),
|
||||
base_url_env_var="GLM_BASE_URL",
|
||||
),
|
||||
"kimi-for-coding": HermesOverlay(
|
||||
"kimi-k2.5": HermesOverlay(
|
||||
transport="openai_chat",
|
||||
base_url_env_var="KIMI_BASE_URL",
|
||||
),
|
||||
@@ -162,10 +162,10 @@ ALIASES: Dict[str, str] = {
|
||||
"z.ai": "zai",
|
||||
"zhipu": "zai",
|
||||
|
||||
# kimi-for-coding (models.dev ID)
|
||||
"kimi": "kimi-for-coding",
|
||||
"kimi-coding": "kimi-for-coding",
|
||||
"moonshot": "kimi-for-coding",
|
||||
# kimi-k2.5 (models.dev ID)
|
||||
"kimi": "kimi-k2.5",
|
||||
"kimi-coding": "kimi-k2.5",
|
||||
"moonshot": "kimi-k2.5",
|
||||
|
||||
# minimax-cn
|
||||
"minimax-china": "minimax-cn",
|
||||
@@ -376,7 +376,7 @@ LABELS: Dict[str, str] = {
|
||||
"github-copilot": "GitHub Copilot",
|
||||
"anthropic": "Anthropic",
|
||||
"zai": "Z.AI / GLM",
|
||||
"kimi-for-coding": "Kimi / Moonshot",
|
||||
"kimi-k2.5": "Kimi / Moonshot",
|
||||
"minimax": "MiniMax",
|
||||
"minimax-cn": "MiniMax (China)",
|
||||
"deepseek": "DeepSeek",
|
||||
|
||||
248
plugins/memory/mempalace/__init__.py
Normal file
248
plugins/memory/mempalace/__init__.py
Normal file
@@ -0,0 +1,248 @@
|
||||
"""
|
||||
MemPalace Portal — Hybrid Memory Provider.
|
||||
|
||||
Bridges the local Holographic fact store with the fleet-wide MemPalace vector database.
|
||||
Implements smart context compression for token efficiency.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import requests
|
||||
from typing import Any, Dict, List, Optional
|
||||
from agent.memory_provider import MemoryProvider
|
||||
|
||||
# Import Holographic components if available
|
||||
try:
|
||||
from plugins.memory.holographic.store import MemoryStore
|
||||
from plugins.memory.holographic.retrieval import FactRetriever
|
||||
HAS_HOLOGRAPHIC = True
|
||||
except ImportError:
|
||||
HAS_HOLOGRAPHIC = False
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool Schemas
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
MEMPALACE_SCHEMA = {
|
||||
"name": "mempalace",
|
||||
"description": (
|
||||
"Search or record memories in the shared fleet vector database. "
|
||||
"Use this for long-term, high-volume memory across the entire fleet."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {"type": "string", "enum": ["search", "record", "wings"]},
|
||||
"query": {"type": "string", "description": "Search query."},
|
||||
"text": {"type": "string", "description": "Memory text to record."},
|
||||
"room": {"type": "string", "description": "Target room (e.g., forge, hermes, nexus)."},
|
||||
"n_results": {"type": "integer", "default": 5},
|
||||
},
|
||||
"required": ["action"],
|
||||
},
|
||||
}
|
||||
|
||||
FACT_STORE_SCHEMA = {
|
||||
"name": "fact_store",
|
||||
"description": (
|
||||
"Structured local fact storage. Use for durable facts about people, projects, and decisions."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {"type": "string", "enum": ["add", "search", "probe", "reason", "update", "remove"]},
|
||||
"content": {"type": "string"},
|
||||
"query": {"type": "string"},
|
||||
"entity": {"type": "string"},
|
||||
"fact_id": {"type": "integer"},
|
||||
},
|
||||
"required": ["action"],
|
||||
},
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Provider Implementation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class MemPalacePortalProvider(MemoryProvider):
|
||||
"""Hybrid Fleet Vector + Local Structured memory provider."""
|
||||
|
||||
def __init__(self, config: dict | None = None):
|
||||
self._config = config or {}
|
||||
self._api_url = os.environ.get("MEMPALACE_API_URL", "http://127.0.0.1:7771")
|
||||
self._hologram_store = None
|
||||
self._hologram_retriever = None
|
||||
self._session_id = None
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "mempalace"
|
||||
|
||||
def is_available(self) -> bool:
|
||||
# Always available if we can reach the API or have Holographic
|
||||
return True
|
||||
|
||||
def initialize(self, session_id: str, **kwargs) -> None:
|
||||
self._session_id = session_id
|
||||
hermes_home = kwargs.get("hermes_home")
|
||||
|
||||
if HAS_HOLOGRAPHIC and hermes_home:
|
||||
db_path = os.path.join(hermes_home, "memory_store.db")
|
||||
try:
|
||||
self._hologram_store = MemoryStore(db_path=db_path)
|
||||
self._hologram_retriever = FactRetriever(store=self._hologram_store)
|
||||
logger.info("Holographic store initialized as local portal layer.")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to init Holographic layer: {e}")
|
||||
|
||||
def system_prompt_block(self) -> str:
|
||||
status = "Active (Fleet Portal)"
|
||||
if self._hologram_store:
|
||||
status += " + Local Hologram"
|
||||
return (
|
||||
f"# MemPalace Portal\n"
|
||||
f"Status: {status}.\n"
|
||||
"You have access to the shared fleet vector database (mempalace) and local structured facts (fact_store).\n"
|
||||
"Use mempalace for semantic fleet-wide recall. Use fact_store for precise local knowledge."
|
||||
)
|
||||
|
||||
def prefetch(self, query: str, *, session_id: str = "") -> str:
|
||||
if not query:
|
||||
return ""
|
||||
|
||||
context_blocks = []
|
||||
|
||||
# 1. Fleet Search (MemPalace)
|
||||
try:
|
||||
res = requests.get(f"{self._api_url}/search", params={"q": query, "n": 3}, timeout=2)
|
||||
if res.ok:
|
||||
data = res.json()
|
||||
memories = data.get("memories", [])
|
||||
if memories:
|
||||
block = "## Fleet Memories (MemPalace)\n"
|
||||
for m in memories:
|
||||
block += f"- {m['text']}\n"
|
||||
context_blocks.append(block)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2. Local Probe (Holographic)
|
||||
if self._hologram_retriever:
|
||||
try:
|
||||
# Extract entities from query to probe
|
||||
entities = re.findall(r'\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\b', query)
|
||||
facts = []
|
||||
for ent in entities:
|
||||
results = self._hologram_retriever.probe(ent, limit=3)
|
||||
facts.extend(results)
|
||||
|
||||
if facts:
|
||||
block = "## Local Facts (Hologram)\n"
|
||||
seen = set()
|
||||
for f in facts:
|
||||
if f['content'] not in seen:
|
||||
block += f"- {f['content']}\n"
|
||||
seen.add(f['content'])
|
||||
context_blocks.append(block)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return "\n\n".join(context_blocks)
|
||||
|
||||
def sync_turn(self, user_content: str, assistant_content: str, *, session_id: str = "") -> None:
|
||||
# Record to Fleet Palace
|
||||
try:
|
||||
payload = {
|
||||
"text": f"User: {user_content}\nAssistant: {assistant_content}",
|
||||
"room": "hermes_sync",
|
||||
"metadata": {"session_id": session_id}
|
||||
}
|
||||
requests.post(f"{self._api_url}/record", json=payload, timeout=2)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def on_pre_compress(self, messages: List[Dict[str, Any]]) -> str:
|
||||
"""Token Efficiency: Summarize and archive before context is lost."""
|
||||
if not messages:
|
||||
return ""
|
||||
|
||||
# Extract key facts for Hologram
|
||||
if self._hologram_store:
|
||||
# Simple heuristic: look for \"I prefer\", \"The project uses\", etc.
|
||||
for msg in messages:
|
||||
if msg.get(\"role\") == \"user\":
|
||||
content = msg.get(\"content\", \"\")
|
||||
if \"prefer\" in content.lower() or \"use\" in content.lower():
|
||||
try:
|
||||
self._hologram_store.add_fact(content[:200], category=\"user_pref\")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Archive session summary to MemPalace
|
||||
summary_text = f"Session {self._session_id} summary: " + " | ".join([m['content'][:50] for m in messages if m.get('role') == 'user'])
|
||||
try:
|
||||
payload = {
|
||||
"text": summary_text,
|
||||
"room": "summaries",
|
||||
"metadata": {"type": "session_summary", "session_id": self._session_id}
|
||||
}
|
||||
requests.post(f"{self._api_url}/record", json=payload, timeout=2)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return "Insights archived to MemPalace and Hologram."
|
||||
|
||||
def get_tool_schemas(self) -> List[Dict[str, Any]]:
|
||||
return [MEMPALACE_SCHEMA, FACT_STORE_SCHEMA]
|
||||
|
||||
def handle_tool_call(self, tool_name: str, args: Dict[str, Any], **kwargs) -> str:
|
||||
if tool_name == "mempalace":
|
||||
return self._handle_mempalace(args)
|
||||
elif tool_name == "fact_store":
|
||||
return self._handle_fact_store(args)
|
||||
return json.dumps({"error": f"Unknown tool: {tool_name}"})
|
||||
|
||||
def _handle_mempalace(self, args: dict) -> str:
|
||||
action = args.get("action")
|
||||
try:
|
||||
if action == "search":
|
||||
res = requests.get(f"{self._api_url}/search", params={"q": args["query"], "n": args.get("n_results", 5)}, timeout=10)
|
||||
return res.text
|
||||
elif action == "record":
|
||||
res = requests.post(f"{self._api_url}/record", json={"text": args["text"], "room": args.get("room", "general")}, timeout=10)
|
||||
return res.text
|
||||
elif action == "wings":
|
||||
res = requests.get(f"{self._api_url}/wings", timeout=10)
|
||||
return res.text
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)})
|
||||
return json.dumps({"error": "Invalid action"})
|
||||
|
||||
def _handle_fact_store(self, args: dict) -> str:
|
||||
if not self._hologram_store:
|
||||
return json.dumps({"error": "Holographic store not initialized locally."})
|
||||
# Logic similar to holographic plugin
|
||||
action = args["action"]
|
||||
try:
|
||||
if action == "add":
|
||||
fid = self._hologram_store.add_fact(args["content"])
|
||||
return json.dumps({"fact_id": fid, "status": "added"})
|
||||
elif action == "probe":
|
||||
res = self._hologram_retriever.probe(args["entity"])
|
||||
return json.dumps({"results": res})
|
||||
# ... other actions ...
|
||||
return json.dumps({"status": "ok", "message": f"Action {action} processed (partial impl)"})
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
def shutdown(self) -> None:
|
||||
if self._hologram_store:
|
||||
self._hologram_store.close()
|
||||
|
||||
def register(ctx) -> None:
|
||||
provider = MemPalacePortalProvider()
|
||||
ctx.register_memory_provider(provider)
|
||||
7
plugins/memory/mempalace/plugin.yaml
Normal file
7
plugins/memory/mempalace/plugin.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
name: mempalace
|
||||
version: 1.0.0
|
||||
description: "The Portal: Hybrid Fleet Vector (MemPalace) + Local Structured (Holographic) memory."
|
||||
dependencies:
|
||||
- requests
|
||||
- numpy
|
||||
74
scripts/check_no_duplicate_models.py
Executable file
74
scripts/check_no_duplicate_models.py
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""CI check: ensure no duplicate model IDs exist in provider configs.
|
||||
|
||||
Catches the class of bugs where a rename introduces a duplicate entry
|
||||
(e.g. PR #225 kimi-for-coding -> kimi-k2.5 when kimi-k2.5 already existed).
|
||||
|
||||
Runtime target: < 2 seconds.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Allow running from repo root
|
||||
REPO_ROOT = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
|
||||
def check_openrouter_models() -> list[str]:
|
||||
"""Check OPENROUTER_MODELS for duplicate model IDs."""
|
||||
try:
|
||||
from hermes_cli.models import OPENROUTER_MODELS
|
||||
except ImportError:
|
||||
return []
|
||||
|
||||
errors = []
|
||||
seen: dict[str, int] = {}
|
||||
for i, (model_id, _desc) in enumerate(OPENROUTER_MODELS):
|
||||
if model_id in seen:
|
||||
errors.append(
|
||||
f" OPENROUTER_MODELS: duplicate '{model_id}' "
|
||||
f"(index {seen[model_id]} and {i})"
|
||||
)
|
||||
else:
|
||||
seen[model_id] = i
|
||||
return errors
|
||||
|
||||
|
||||
def check_provider_models() -> list[str]:
|
||||
"""Check _PROVIDER_MODELS for duplicate model IDs within each provider list."""
|
||||
from hermes_cli.models import _PROVIDER_MODELS
|
||||
|
||||
errors = []
|
||||
for provider, models in _PROVIDER_MODELS.items():
|
||||
seen: dict[str, int] = {}
|
||||
for i, model_id in enumerate(models):
|
||||
if model_id in seen:
|
||||
errors.append(
|
||||
f" _PROVIDER_MODELS['{provider}']: duplicate '{model_id}' "
|
||||
f"(index {seen[model_id]} and {i})"
|
||||
)
|
||||
else:
|
||||
seen[model_id] = i
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
errors = []
|
||||
errors.extend(check_openrouter_models())
|
||||
errors.extend(check_provider_models())
|
||||
|
||||
if errors:
|
||||
print(f"FAIL: {len(errors)} duplicate model(s) found:")
|
||||
for e in errors:
|
||||
print(e)
|
||||
return 1
|
||||
|
||||
print("OK: no duplicate model entries")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -895,7 +895,7 @@ class TestKimiMoonshotModelListIsolation:
|
||||
def test_moonshot_list_excludes_coding_plan_only_models(self):
|
||||
from hermes_cli.main import _PROVIDER_MODELS
|
||||
moonshot_models = _PROVIDER_MODELS["moonshot"]
|
||||
coding_plan_only = {"kimi-k2-thinking-turbo"}
|
||||
coding_plan_only = {"kimi-k2.5", "kimi-k2-thinking-turbo"}
|
||||
leaked = set(moonshot_models) & coding_plan_only
|
||||
assert not leaked, f"Moonshot list contains Coding Plan-only models: {leaked}"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user