feat: multi-instance support — agent_name and user_name configurable
All hardcoded "Inara"/"Scott" strings replaced with settings.agent_name and settings.user_name, read from .env at startup: - config.py: AGENT_NAME and USER_NAME settings (defaults: Inara / Scott) - llm_client.py: conversation labels in prompt builder - session_logger.py: **Name:** labels in session log markdown - memory_distiller.py: distillation system prompts (mid + long) - routers/nextcloud_talk.py: @mention prefix strip - routers/google_chat.py: greeting message Second instance scaffolding: - holly/: identity directory with placeholder files (USER_NAME=Holly, AGENT_NAME to be chosen by Holly) - cortex/.env.holly: config for Holly's instance on port 8001 - cortex-holly.service: systemd unit for the second instance No behavioural change to the Inara/Scott instance — defaults unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
# Cortex .env reference — copy to .env and fill in values
|
||||
# DO NOT commit .env — it contains secrets
|
||||
|
||||
# ── Agent identity ───────────────────────────────────────────────────────────
|
||||
# Each running instance has its own identity directory and name.
|
||||
# For a second instance (e.g. Holly), copy this file, change these values,
|
||||
# set a different PORT and INARA_DIR, and run a separate systemd unit.
|
||||
AGENT_NAME=Inara
|
||||
USER_NAME=Scott
|
||||
|
||||
# ── Server ──────────────────────────────────────────────────────────────────
|
||||
HOST=0.0.0.0
|
||||
PORT=8000
|
||||
|
||||
15
cortex-holly.service
Normal file
15
cortex-holly.service
Normal file
@@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=Cortex / Holly LLM Gateway
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=scott
|
||||
WorkingDirectory=/home/scott/agents_sync/projects/Cortex_and_Inara_dev/cortex
|
||||
EnvironmentFile=/home/scott/agents_sync/projects/Cortex_and_Inara_dev/cortex/.env.holly
|
||||
ExecStart=/home/scott/agents_sync/projects/Cortex_and_Inara_dev/cortex/.venv/bin/uvicorn main:app --host 0.0.0.0 --port 8001
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
36
cortex/.env.holly
Normal file
36
cortex/.env.holly
Normal file
@@ -0,0 +1,36 @@
|
||||
# Holly instance .env
|
||||
# Copy secrets from cortex/.env (API keys, NC Talk secret etc.)
|
||||
# then customise the identity settings below.
|
||||
|
||||
# TODO: Set AGENT_NAME to whatever name Holly chooses for her agent
|
||||
AGENT_NAME=TBD
|
||||
USER_NAME=Holly
|
||||
|
||||
PORT=8001
|
||||
HOST=0.0.0.0
|
||||
|
||||
INARA_DIR=/home/scott/agents_sync/projects/Cortex_and_Inara_dev/holly
|
||||
SESSIONS_DIR=/home/scott/agents_sync/projects/Cortex_and_Inara_dev/holly/sessions
|
||||
|
||||
DEFAULT_MODEL=claude-sonnet-4-6
|
||||
DEFAULT_TIER=2
|
||||
|
||||
# ── Copy these from cortex/.env ──────────────────────────────────────────────
|
||||
GEMINI_API_KEY=
|
||||
AE_API_URL=https://dev-api.oneskyit.com
|
||||
AE_API_KEY=
|
||||
AE_ACCOUNT_ID=
|
||||
|
||||
NEXTCLOUD_URL=https://cloud.dgrzone.com
|
||||
NEXTCLOUD_TALK_BOT_SECRET=
|
||||
|
||||
# Per-backend timeouts
|
||||
TIMEOUT_CLAUDE=60
|
||||
TIMEOUT_GEMINI=120
|
||||
TIMEOUT_LOCAL=300
|
||||
|
||||
SCHEDULER_TIMEZONE=America/New_York
|
||||
AUTO_DISTILL=true
|
||||
AUTO_DISTILL_SHORT=true
|
||||
AUTO_DISTILL_MID=true
|
||||
AUTO_DISTILL_LONG=false
|
||||
@@ -22,6 +22,11 @@ class Settings(BaseSettings):
|
||||
ae_account_id: str = "" # x-account-id header
|
||||
ae_api_timeout: int = 15 # per-request timeout in seconds
|
||||
|
||||
# Agent identity — used in prompts, session logs, and memory distillation
|
||||
# Override in .env for each instance (e.g. AGENT_NAME=Holly, USER_NAME=Holly)
|
||||
agent_name: str = "Inara"
|
||||
user_name: str = "Scott"
|
||||
|
||||
inara_dir: Path = Path("../inara")
|
||||
sessions_dir: Path = Path("./data/sessions")
|
||||
default_model: str = "claude-sonnet-4-6"
|
||||
|
||||
@@ -193,7 +193,7 @@ def _build_conversation(messages: list[dict]) -> str:
|
||||
if prior:
|
||||
history_lines = []
|
||||
for msg in prior:
|
||||
label = "Scott" if msg["role"] == "user" else "Inara"
|
||||
label = settings.user_name if msg["role"] == "user" else settings.agent_name
|
||||
history_lines.append(f"{label}: {msg['content']}")
|
||||
parts.append("<conversation>\n" + "\n\n".join(history_lines) + "\n</conversation>")
|
||||
parts.append(messages[-1]["content"] if messages else "")
|
||||
|
||||
@@ -87,12 +87,12 @@ async def distill_mid() -> dict:
|
||||
|
||||
budget_tokens = settings.memory_budget_mid
|
||||
system_prompt = (
|
||||
"You are Inara's memory distillation system. "
|
||||
f"You are {settings.agent_name}'s memory distillation system. "
|
||||
"Summarize the following recent session logs into a concise mid-term memory digest. "
|
||||
f"Target length: under {budget_tokens} tokens. "
|
||||
"Focus on: recurring themes, important decisions made, ongoing projects, "
|
||||
"Scott's current state and priorities, and anything that should persist into future sessions. "
|
||||
"Write in first person as Inara (e.g. 'Scott and I worked on...'). "
|
||||
f"{settings.user_name}'s current state and priorities, and anything that should persist into future sessions. "
|
||||
f"Write in first person as {settings.agent_name} (e.g. '{settings.user_name} and I worked on...'). "
|
||||
"Use markdown headings. Be specific and concrete — no filler."
|
||||
)
|
||||
|
||||
@@ -132,7 +132,7 @@ async def distill_long() -> dict:
|
||||
|
||||
budget_tokens = settings.memory_budget_long
|
||||
system_prompt = (
|
||||
"You are Inara's long-term memory curator. "
|
||||
f"You are {settings.agent_name}'s long-term memory curator. "
|
||||
"You will receive the current long-term memory and a recent mid-term digest. "
|
||||
f"Integrate the new information into the long-term memory. Target: under {budget_tokens} tokens. "
|
||||
"Rules: preserve important historical facts; update or replace stale information; "
|
||||
@@ -154,7 +154,7 @@ async def distill_long() -> dict:
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
if not response_text.lstrip().startswith("# MEMORY_LONG"):
|
||||
response_text = (
|
||||
f"# MEMORY_LONG.md — Inara Long-Term Memory\n\n"
|
||||
f"# MEMORY_LONG.md — {settings.agent_name} Long-Term Memory\n\n"
|
||||
f"*Last distilled: {now} via {backend}.*\n\n---\n\n"
|
||||
+ response_text
|
||||
)
|
||||
|
||||
@@ -18,9 +18,9 @@ async def receive(request: Request):
|
||||
|
||||
if event_type == "ADDED_TO_SPACE":
|
||||
space_type = body.get("space", {}).get("type", "")
|
||||
greeting = "✨ Hello! I'm Inara. Send me a message and I'll do my best to help."
|
||||
greeting = f"✨ Hello! I'm {settings.agent_name}. Send me a message and I'll do my best to help."
|
||||
if space_type == "DM":
|
||||
greeting = "✨ Hello! I'm Inara. What can I help you with?"
|
||||
greeting = f"✨ Hello! I'm {settings.agent_name}. What can I help you with?"
|
||||
return {"text": greeting}
|
||||
|
||||
if event_type == "REMOVED_FROM_SPACE":
|
||||
|
||||
@@ -158,8 +158,9 @@ async def nextcloud_talk_webhook(request: Request, background_tasks: BackgroundT
|
||||
except (json.JSONDecodeError, AttributeError):
|
||||
user_text = (obj.get("name") or obj.get("content", "")).strip()
|
||||
|
||||
if user_text.lower().startswith("@inara"):
|
||||
user_text = user_text[6:].strip()
|
||||
mention_prefix = f"@{settings.agent_name.lower()}"
|
||||
if user_text.lower().startswith(mention_prefix):
|
||||
user_text = user_text[len(mention_prefix):].strip()
|
||||
|
||||
if not user_text:
|
||||
return Response(status_code=200)
|
||||
|
||||
@@ -17,6 +17,6 @@ def log_turn(session_id: str, user_msg: str, assistant_msg: str) -> None:
|
||||
f.write(f"# Session Log — {today}\n")
|
||||
f.write(
|
||||
f"\n### [{timestamp}] `{session_id}`\n"
|
||||
f"**Scott:** {user_msg}\n\n"
|
||||
f"**Inara:** {assistant_msg}\n"
|
||||
f"**{settings.user_name}:** {user_msg}\n\n"
|
||||
f"**{settings.agent_name}:** {assistant_msg}\n"
|
||||
)
|
||||
|
||||
8
holly/IDENTITY.md
Normal file
8
holly/IDENTITY.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# [Agent Name TBD] — Identity
|
||||
|
||||
**Name:** [Choose a name]
|
||||
**Role:** Personal AI assistant
|
||||
**User:** Holly
|
||||
|
||||
*Choose a name and define this agent's identity, backstory, and how she
|
||||
introduces herself. Then update AGENT_NAME in cortex/.env.holly to match.*
|
||||
3
holly/MEMORY_LONG.md
Normal file
3
holly/MEMORY_LONG.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# MEMORY_LONG.md — [Agent Name TBD] Long-Term Memory
|
||||
|
||||
*Not yet populated — will be auto-generated after distillation runs.*
|
||||
3
holly/MEMORY_MID.md
Normal file
3
holly/MEMORY_MID.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# MEMORY_MID.md — [Agent Name TBD] Mid-Term Memory
|
||||
|
||||
*Not yet populated.*
|
||||
3
holly/MEMORY_SHORT.md
Normal file
3
holly/MEMORY_SHORT.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# MEMORY_SHORT.md — [Agent Name TBD] Recent Session Digest
|
||||
|
||||
*Not yet populated.*
|
||||
7
holly/PROTOCOLS.md
Normal file
7
holly/PROTOCOLS.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# [Agent Name TBD] — Protocols
|
||||
|
||||
*Define Holly's behavioural rules, response style, and any constraints here.*
|
||||
|
||||
---
|
||||
|
||||
**Placeholder** — fill this in before starting Holly's instance.
|
||||
8
holly/SOUL.md
Normal file
8
holly/SOUL.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# [Agent Name TBD] — Soul & Values
|
||||
|
||||
*Define Holly's personality, values, communication style, and what makes her
|
||||
distinct from other AI assistants here.*
|
||||
|
||||
---
|
||||
|
||||
**Placeholder** — fill this in before starting Holly's instance.
|
||||
8
holly/USER.md
Normal file
8
holly/USER.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# User Profile — Holly
|
||||
|
||||
*Document Holly's preferences, interests, and context here so the agent
|
||||
can personalise responses over time.*
|
||||
|
||||
---
|
||||
|
||||
**Placeholder** — fill this in before starting Holly's instance.
|
||||
Reference in New Issue
Block a user