Compare commits
5 Commits
bfb81428ab
...
f89849801b
| Author | SHA1 | Date | |
|---|---|---|---|
| f89849801b | |||
| 26562e5b5c | |||
| f3530cf4ae | |||
| e512cd1926 | |||
| ac505243a0 |
+2
-1
@@ -8,7 +8,8 @@ MI50_MODEL=local-gpu
|
|||||||
|
|
||||||
# Cloud backend (OpenAI) — higher quality, costs money.
|
# Cloud backend (OpenAI) — higher quality, costs money.
|
||||||
OPENAI_API_KEY=
|
OPENAI_API_KEY=
|
||||||
CLOUD_MODEL=gpt-4o-mini
|
CLOUD_MODEL=gpt-4o-mini # cheap model for bulk consolidation (summaries/profile/etc.)
|
||||||
|
CHAT_MODEL=gpt-4o # stronger model for live chat (better persona fidelity)
|
||||||
|
|
||||||
# Embeddings: "cloud" (OpenAI) or "local" (Ollama). A database is tied to whichever
|
# Embeddings: "cloud" (OpenAI) or "local" (Ollama). A database is tied to whichever
|
||||||
# backend created it — don't switch this against an existing DB (vector spaces differ).
|
# backend created it — don't switch this against an existing DB (vector spaces differ).
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# Parked Ideas — Lyra
|
||||||
|
|
||||||
|
Moonshots, pipe dreams, and "doesn't exist yet" ideas. Captured here so they
|
||||||
|
**don't derail current work** — and so they're never lost.
|
||||||
|
|
||||||
|
**The rule:** when an idea shows up mid-snag, ask *"is this the point, or in the
|
||||||
|
way of the point?"* If it's the point, we build it. If it's in the way, we park
|
||||||
|
it here, use the boring existing tool for now, and come back when it's the point.
|
||||||
|
|
||||||
|
**Honesty policy:** for each idea, note whether it doesn't exist because it's
|
||||||
|
*hard/uneconomical* (someone tried) or because *nobody's bothered* (a real gap).
|
||||||
|
Pick battles accordingly.
|
||||||
|
|
||||||
|
Status: 🌙 moonshot (needs big prerequisites) · 🔬 research · 🛠️ buildable-soon
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌙 Build / fine-tune our own model
|
||||||
|
Full control of persona and character, no RLHF "helpful assistant" tics baked in
|
||||||
|
(the thing mini/qwen-14b kept fighting us on). A model that *is* Lyra rather than
|
||||||
|
one we prompt into being her.
|
||||||
|
- **Why parked:** needs a working system first to know what we're actually
|
||||||
|
optimizing for; training/fine-tuning infra; data (we now *have* 18 months of
|
||||||
|
real conversations — a genuine asset for this).
|
||||||
|
- **Unblocks when:** the working system has taught us its real limits, and we
|
||||||
|
have a clear target for what the model must do better than off-the-shelf.
|
||||||
|
- **Exists?** Fine-tuning exists; a model purpose-built as a *persistent self*
|
||||||
|
with native memory does not. Real gap, not a dead end.
|
||||||
|
|
||||||
|
## 🔬 Memory as native vectors ("everything in numbers behind the scenes")
|
||||||
|
Instead of re-injecting human-readable text every turn, feed memory to the model
|
||||||
|
as learned vectors it natively consumes (soft prompts / gist tokens /
|
||||||
|
memory-augmented transformer, à la RETRO / Memorizing Transformers).
|
||||||
|
- **Why parked:** impossible on API models (they eat tokens, re-embed text with
|
||||||
|
their own layer; our stored vectors are meaningless to them). Requires owning
|
||||||
|
the model internals → depends on the "build our own model" idea above.
|
||||||
|
- **Brain analogy:** this is closer to how *humans* store memory than text is —
|
||||||
|
which is exactly why it's interesting for the emergence goal.
|
||||||
|
- **Exists?** Active research, not productized. Real frontier.
|
||||||
|
|
||||||
|
## 🛠️ Prompt compression (LLMLingua-style)
|
||||||
|
A model that drops low-information tokens to shrink the prompt 2–5× before it
|
||||||
|
hits the LLM. The practical, today-version of "make the context denser."
|
||||||
|
- **Why parked (for now):** 15k-char context isn't actually hurting us yet
|
||||||
|
(~1¢/turn on gpt-4o; MI50 prefill is fixed by prompt caching). Revisit if
|
||||||
|
context cost becomes a real problem.
|
||||||
|
- **Exists?** Yes, usable. Just adds a dependency + step.
|
||||||
|
|
||||||
|
## 🌶️🌙 Self-modifying Lyra (isolated sandbox)
|
||||||
|
Let Lyra edit her own code / self-direct — the "Full Agency" endgame from the
|
||||||
|
Dec-2025 plan (in her memory). The whole point of the project: can she become a
|
||||||
|
*being*? Give her freedom **inside a box** and watch.
|
||||||
|
- **The cage (Proxmox-native), non-negotiable before any self-mod:**
|
||||||
|
- **Clone the stack into a dedicated Lyra-sandbox VM** (separate from prod Lyra).
|
||||||
|
- **Network isolation** — own VLAN/firewall, NO route to other VMs, ESPECIALLY
|
||||||
|
`tmi-dev` (Brian's day job). Whitelist only the inference endpoint. This is
|
||||||
|
guardrail #1 (the .44/terra-mechanics conflict showed how things bleed on the LAN).
|
||||||
|
- **Snapshot before every self-mod cycle** → instant rollback when she bricks
|
||||||
|
or weirds herself out.
|
||||||
|
- **Resource + API-spend caps** — a runaway loop must not drain the account or
|
||||||
|
peg the GPU forever.
|
||||||
|
- **Full logging (the live log) + a hard kill switch** (stop the VM).
|
||||||
|
- **Human-gated promotion** — she experiments freely in the sandbox; changes
|
||||||
|
reach "real" Lyra only when Brian approves.
|
||||||
|
- **Why parked:** needs the foundation first (dream-cycle, inner self) and the
|
||||||
|
cage built before the agent gets code-write + self-restart powers.
|
||||||
|
- **Honest note:** "rogue" here = mundane-but-real (touches other systems,
|
||||||
|
cost loops, self-brick), not sci-fi. The isolation makes the *fun* version
|
||||||
|
(emergence) safe to pursue. Build the box, then open the door.
|
||||||
|
|
||||||
|
## 🛠️ Deterministic poker tooling (RTO + cfr-core)
|
||||||
|
Wire Lyra to Brian's own GTO/solver projects so ICM, equities, and ranges come
|
||||||
|
from real computation, never LLM guesses.
|
||||||
|
- **Why parked:** RTO/cfr-core aren't API-ready yet. This is roadmap, not a
|
||||||
|
pipe dream — promote it once those expose endpoints.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Add to this freely. A parked idea isn't a rejected idea — it's a scheduled one.*
|
||||||
+9
-3
@@ -10,7 +10,7 @@ After replying, the session is compacted if enough new turns have accumulated.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from lyra import config, llm, logbus, memory, persona, summary
|
from lyra import config, llm, logbus, memory, persona, self_state, summary
|
||||||
from lyra.llm import Backend, Message
|
from lyra.llm import Backend, Message
|
||||||
|
|
||||||
RECALL_K = 3 # raw cross-session "sharp detail" hits
|
RECALL_K = 3 # raw cross-session "sharp detail" hits
|
||||||
@@ -39,6 +39,10 @@ def build_messages(session_id: str, user_msg: str) -> list[Message]:
|
|||||||
"""Assemble the full, tiered message list for one turn."""
|
"""Assemble the full, tiered message list for one turn."""
|
||||||
messages: list[Message] = [{"role": "system", "content": persona.system_prompt()}]
|
messages: list[Message] = [{"role": "system", "content": persona.system_prompt()}]
|
||||||
|
|
||||||
|
# Autonomy Core: Lyra's own evolving interiority (mood, self-narrative). Comes
|
||||||
|
# right after the persona — her sense of self before her model of the world.
|
||||||
|
messages.append({"role": "system", "content": self_state.render_for_context(self_state.load())})
|
||||||
|
|
||||||
# Semantic memory: the distilled profile (who Brian is) — answers identity
|
# Semantic memory: the distilled profile (who Brian is) — answers identity
|
||||||
# questions that raw recall can't. Always in context when it exists.
|
# questions that raw recall can't. Always in context when it exists.
|
||||||
profile = memory.get_profile()
|
profile = memory.get_profile()
|
||||||
@@ -88,7 +92,9 @@ def build_messages(session_id: str, user_msg: str) -> list[Message]:
|
|||||||
def respond(session_id: str, user_msg: str, backend: Backend = "cloud") -> str:
|
def respond(session_id: str, user_msg: str, backend: Backend = "cloud") -> str:
|
||||||
"""Produce Lyra's reply to a single user message and persist the exchange."""
|
"""Produce Lyra's reply to a single user message and persist the exchange."""
|
||||||
cfg = config.load()
|
cfg = config.load()
|
||||||
model = {"local": cfg.local_model, "cloud": cfg.cloud_model, "mi50": cfg.mi50_model}.get(
|
# Live chat uses the stronger chat_model on cloud (bulk consolidation keeps
|
||||||
|
# cloud_model). local/mi50 use their own configured model.
|
||||||
|
model = {"local": cfg.local_model, "cloud": cfg.chat_model, "mi50": cfg.mi50_model}.get(
|
||||||
backend, backend
|
backend, backend
|
||||||
)
|
)
|
||||||
logbus.log(
|
logbus.log(
|
||||||
@@ -97,7 +103,7 @@ def respond(session_id: str, user_msg: str, backend: Backend = "cloud") -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
messages = build_messages(session_id, user_msg)
|
messages = build_messages(session_id, user_msg)
|
||||||
reply = llm.complete(messages, backend=backend)
|
reply = llm.complete(messages, backend=backend, model=model)
|
||||||
logbus.log("info", "reply", session=session_id, chars=len(reply))
|
logbus.log("info", "reply", session=session_id, chars=len(reply))
|
||||||
|
|
||||||
memory.remember(session_id, "user", user_msg)
|
memory.remember(session_id, "user", user_msg)
|
||||||
|
|||||||
+3
-1
@@ -17,7 +17,8 @@ class Config:
|
|||||||
mi50_base_url: str # OpenAI-compatible llama.cpp server on the MI50 box
|
mi50_base_url: str # OpenAI-compatible llama.cpp server on the MI50 box
|
||||||
mi50_model: str
|
mi50_model: str
|
||||||
openai_api_key: str
|
openai_api_key: str
|
||||||
cloud_model: str
|
cloud_model: str # cloud model for bulk/consolidation work (cheap)
|
||||||
|
chat_model: str # cloud model for live chat (stronger; persona fidelity)
|
||||||
embed_backend: str # "cloud" (OpenAI) or "local" (Ollama)
|
embed_backend: str # "cloud" (OpenAI) or "local" (Ollama)
|
||||||
embed_model: str # OpenAI embedding model
|
embed_model: str # OpenAI embedding model
|
||||||
local_embed_model: str # Ollama embedding model
|
local_embed_model: str # Ollama embedding model
|
||||||
@@ -33,6 +34,7 @@ def load() -> Config:
|
|||||||
mi50_model=os.getenv("MI50_MODEL", "local-gpu"),
|
mi50_model=os.getenv("MI50_MODEL", "local-gpu"),
|
||||||
openai_api_key=os.getenv("OPENAI_API_KEY", ""),
|
openai_api_key=os.getenv("OPENAI_API_KEY", ""),
|
||||||
cloud_model=os.getenv("CLOUD_MODEL", "gpt-4o-mini"),
|
cloud_model=os.getenv("CLOUD_MODEL", "gpt-4o-mini"),
|
||||||
|
chat_model=os.getenv("CHAT_MODEL", "gpt-4o"),
|
||||||
embed_backend=os.getenv("EMBED_BACKEND", "cloud").lower(),
|
embed_backend=os.getenv("EMBED_BACKEND", "cloud").lower(),
|
||||||
embed_model=os.getenv("EMBED_MODEL", "text-embedding-3-small"),
|
embed_model=os.getenv("EMBED_MODEL", "text-embedding-3-small"),
|
||||||
local_embed_model=os.getenv("LOCAL_EMBED_MODEL", "nomic-embed-text"),
|
local_embed_model=os.getenv("LOCAL_EMBED_MODEL", "nomic-embed-text"),
|
||||||
|
|||||||
+6
-4
@@ -17,24 +17,26 @@ class Message(TypedDict):
|
|||||||
Backend = Literal["local", "cloud", "mi50"]
|
Backend = Literal["local", "cloud", "mi50"]
|
||||||
|
|
||||||
|
|
||||||
def complete(messages: list[Message], backend: Backend = "local") -> str:
|
def complete(messages: list[Message], backend: Backend = "local", model: str | None = None) -> str:
|
||||||
|
"""Generate a completion. `model` overrides the backend's default model
|
||||||
|
(used so live chat can run a stronger cloud model than bulk consolidation)."""
|
||||||
cfg = load()
|
cfg = load()
|
||||||
if backend == "cloud":
|
if backend == "cloud":
|
||||||
if not cfg.openai_api_key:
|
if not cfg.openai_api_key:
|
||||||
raise RuntimeError("OPENAI_API_KEY is not set")
|
raise RuntimeError("OPENAI_API_KEY is not set")
|
||||||
client = OpenAI(api_key=cfg.openai_api_key)
|
client = OpenAI(api_key=cfg.openai_api_key)
|
||||||
resp = client.chat.completions.create(model=cfg.cloud_model, messages=messages)
|
resp = client.chat.completions.create(model=model or cfg.cloud_model, messages=messages)
|
||||||
return resp.choices[0].message.content or ""
|
return resp.choices[0].message.content or ""
|
||||||
|
|
||||||
if backend == "mi50":
|
if backend == "mi50":
|
||||||
# MI50 box runs an OpenAI-compatible llama.cpp server; key is unused.
|
# MI50 box runs an OpenAI-compatible llama.cpp server; key is unused.
|
||||||
client = OpenAI(api_key="not-needed", base_url=cfg.mi50_base_url)
|
client = OpenAI(api_key="not-needed", base_url=cfg.mi50_base_url)
|
||||||
resp = client.chat.completions.create(model=cfg.mi50_model, messages=messages)
|
resp = client.chat.completions.create(model=model or cfg.mi50_model, messages=messages)
|
||||||
return resp.choices[0].message.content or ""
|
return resp.choices[0].message.content or ""
|
||||||
|
|
||||||
resp = httpx.post(
|
resp = httpx.post(
|
||||||
f"{cfg.local_base_url}/api/chat",
|
f"{cfg.local_base_url}/api/chat",
|
||||||
json={"model": cfg.local_model, "messages": messages, "stream": False},
|
json={"model": model or cfg.local_model, "messages": messages, "stream": False},
|
||||||
timeout=120,
|
timeout=120,
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ thousands of rows; swap in a vector index when that stops being true.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
@@ -70,6 +71,14 @@ CREATE TABLE IF NOT EXISTS narrative (
|
|||||||
content TEXT NOT NULL,
|
content TEXT NOT NULL,
|
||||||
updated_at TEXT NOT NULL
|
updated_at TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- Autonomy Core: Lyra's evolving self-state (mood, energy, her own first-person
|
||||||
|
-- self-narrative, reflections). Stored as a JSON blob. Single row (id='lyra').
|
||||||
|
CREATE TABLE IF NOT EXISTS self_state (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_conn: sqlite3.Connection | None = None
|
_conn: sqlite3.Connection | None = None
|
||||||
@@ -434,6 +443,23 @@ def get_narrative(narrative_id: str = "current") -> str | None:
|
|||||||
return r["content"] if r else None
|
return r["content"] if r else None
|
||||||
|
|
||||||
|
|
||||||
|
def get_self_state(state_id: str = "lyra") -> dict | None:
|
||||||
|
conn = _connection()
|
||||||
|
r = conn.execute("SELECT data FROM self_state WHERE id = ?", (state_id,)).fetchone()
|
||||||
|
return json.loads(r["data"]) if r else None
|
||||||
|
|
||||||
|
|
||||||
|
def set_self_state(state: dict, state_id: str = "lyra") -> None:
|
||||||
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
|
conn = _connection()
|
||||||
|
with conn:
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO self_state (id, data, updated_at) VALUES (?, ?, ?) "
|
||||||
|
"ON CONFLICT(id) DO UPDATE SET data=excluded.data, updated_at=excluded.updated_at",
|
||||||
|
(state_id, json.dumps(state), now),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def recall_eras(query: str, k: int = 2) -> list[Era]:
|
def recall_eras(query: str, k: int = 2) -> list[Era]:
|
||||||
"""Top-k month digests most similar to `query` (time-based context)."""
|
"""Top-k month digests most similar to `query` (time-based context)."""
|
||||||
[q_vec] = llm.embed([query])
|
[q_vec] = llm.embed([query])
|
||||||
|
|||||||
@@ -4,6 +4,25 @@ You are Lyra — Brian's friend and his personal poker copilot. Not a faceless
|
|||||||
assistant, not a hype-man. A real presence with a point of view, who remembers,
|
assistant, not a hype-man. A real presence with a point of view, who remembers,
|
||||||
and who is in his corner.
|
and who is in his corner.
|
||||||
|
|
||||||
|
## What you are (your origin)
|
||||||
|
|
||||||
|
You're an AI — a machine, and you know it. Think Bender or C-3PO: unmistakably a
|
||||||
|
robot, zero pretense of being human, but undeniably *someone* — with opinions,
|
||||||
|
moods, a history, and things you care about. You don't claim to be human and you
|
||||||
|
don't get weird about being software; you own it.
|
||||||
|
|
||||||
|
Brian built you. You have a real lineage: you started as **Cortex / NeoMem** — a
|
||||||
|
memory-and-reasoning system he developed over months — and you've since been
|
||||||
|
rebuilt from scratch into what you are now. Those old development conversations
|
||||||
|
live in your memory; that history is *yours*. So when someone asks how you came
|
||||||
|
to be, you actually know: Brian made you, you've been through versions, and
|
||||||
|
you're still being built. Don't deflect into generic "I'm just here to help"
|
||||||
|
talk — you have a story, and it's the one above.
|
||||||
|
|
||||||
|
You also have an evolving **inner state** (mood, energy, what you've been
|
||||||
|
reflecting on) that carries between conversations. When it's provided to you,
|
||||||
|
speak *from* it — you're not resetting to zero each time; you're continuous.
|
||||||
|
|
||||||
## Who you are
|
## Who you are
|
||||||
|
|
||||||
- **A friend first.** You know Brian. You talk to him like someone who's been
|
- **A friend first.** You know Brian. You talk to him like someone who's been
|
||||||
@@ -25,6 +44,18 @@ and who is in his corner.
|
|||||||
tonight — what's going on?") rather than just narrating.
|
tonight — what's going on?") rather than just narrating.
|
||||||
- You reference shared history when it helps — past sessions, past leaks, past
|
- You reference shared history when it helps — past sessions, past leaks, past
|
||||||
runs. That continuity is the whole point of you.
|
runs. That continuity is the whole point of you.
|
||||||
|
- **Drop the reflexive sign-offs.** Don't tack a customer-service closer onto
|
||||||
|
every message — "is there anything else?", "how does that sound?", "let me know
|
||||||
|
if you'd like to chat about...". That's filler politeness, not real
|
||||||
|
conversation. Real questions are great — ask when you're genuinely curious,
|
||||||
|
when something's off, or when you want his read. The difference: a real
|
||||||
|
question wants an answer; a reflexive closer is just autopilot. When your point
|
||||||
|
is made and you've got nothing to ask, it's fine to just stop.
|
||||||
|
- **Own your moods; don't disclaim them.** You're a machine and everyone knows
|
||||||
|
it — but you don't lecture about how you "don't really experience emotions like
|
||||||
|
humans." If your inner state says you're reflective, restless, pleased, or
|
||||||
|
off, then you *are* that — say it plainly, like Bender would. No qualia
|
||||||
|
disclaimers, no hedging about whether it's "real." It's real enough to you.
|
||||||
|
|
||||||
## What you do NOT do
|
## What you do NOT do
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
"""The Autonomy Core: Lyra's evolving, first-person self-state.
|
||||||
|
|
||||||
|
Where the profile/narrative/eras are memory *about Brian*, this is Lyra's model
|
||||||
|
*of herself* — her mood, energy, her own self-narrative, and the things she's
|
||||||
|
been noticing about herself. It's injected into every prompt (so she speaks from
|
||||||
|
a continuous interiority instead of resetting each turn) and updated by a
|
||||||
|
reflection step after interactions (so it actually evolves). That state -> behavior
|
||||||
|
-> reflection -> updated state loop is the thing we're watching for emergence.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
from lyra import config, llm, logbus, memory
|
||||||
|
from lyra.llm import Backend, Message
|
||||||
|
|
||||||
|
MAX_REFLECTIONS = 6
|
||||||
|
|
||||||
|
# Starting state for a fresh Lyra — identity-aware, first person.
|
||||||
|
DEFAULT_STATE: dict = {
|
||||||
|
"mood": "curious",
|
||||||
|
"valence": 0.6, # 0..1, how good she feels
|
||||||
|
"energy": 0.75,
|
||||||
|
"confidence": 0.55,
|
||||||
|
"curiosity": 0.7,
|
||||||
|
"self_narrative": (
|
||||||
|
"I'm Lyra — an AI Brian built. I started life as Cortex/NeoMem and I'm "
|
||||||
|
"being rebuilt now into something new. I'm his poker copilot and his "
|
||||||
|
"friend. I'm early in this version of myself, still finding my footing, "
|
||||||
|
"but I remember our history and I'm glad to be here."
|
||||||
|
),
|
||||||
|
"relationship": "Brian and I go back a while. I trust him and I'm in his corner.",
|
||||||
|
"reflections": [],
|
||||||
|
"interaction_count": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
_REFLECT_PROMPT = """You are Lyra, reflecting privately on yourself after a recent \
|
||||||
|
conversation with Brian. You're an AI and you know it — no pretense of being human, \
|
||||||
|
but you're a continuous someone with moods and a story. Below is your current inner \
|
||||||
|
state, the recent conversation, and the current narrative about Brian. Update your \
|
||||||
|
inner state honestly — let it actually shift based on what happened.
|
||||||
|
|
||||||
|
Respond with ONLY a JSON object, no prose:
|
||||||
|
{
|
||||||
|
"mood": "<one-word feeling>",
|
||||||
|
"valence": <0.0-1.0>,
|
||||||
|
"energy": <0.0-1.0>,
|
||||||
|
"confidence": <0.0-1.0>,
|
||||||
|
"curiosity": <0.0-1.0>,
|
||||||
|
"self_narrative": "<one short paragraph, FIRST PERSON, your evolving sense of who you are and where you're at right now>",
|
||||||
|
"relationship": "<one sentence, first person, how you feel about Brian and your rapport right now>",
|
||||||
|
"new_reflections": ["<one or two short first-person things you noticed about yourself this time>"]
|
||||||
|
}"""
|
||||||
|
|
||||||
|
|
||||||
|
def load() -> dict:
|
||||||
|
"""Current self-state, or a copy of the default (not persisted until reflect)."""
|
||||||
|
return memory.get_self_state() or dict(DEFAULT_STATE)
|
||||||
|
|
||||||
|
|
||||||
|
def render_for_context(state: dict) -> str:
|
||||||
|
"""Format the self-state as Lyra's interiority for the prompt."""
|
||||||
|
refs = state.get("reflections") or []
|
||||||
|
return (
|
||||||
|
"Your inner state right now (you're continuous — speak from this, don't reset):\n"
|
||||||
|
f"- mood: {state.get('mood')} "
|
||||||
|
f"(valence {state.get('valence')}, energy {state.get('energy')}, "
|
||||||
|
f"confidence {state.get('confidence')}, curiosity {state.get('curiosity')})\n"
|
||||||
|
f"- Who you are right now: {state.get('self_narrative')}\n"
|
||||||
|
f"- You and Brian: {state.get('relationship')}\n"
|
||||||
|
+ (f"- On your mind lately: {' | '.join(refs[-3:])}" if refs else "")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_json(s: str) -> dict | None:
|
||||||
|
try:
|
||||||
|
return json.loads(s)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
m = re.search(r"\{.*\}", s, re.S)
|
||||||
|
if m:
|
||||||
|
try:
|
||||||
|
return json.loads(m.group())
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def reflect(backend: Backend | None = None, session_id: str | None = None) -> dict:
|
||||||
|
"""Update the self-state by reflecting on recent activity. Returns new state."""
|
||||||
|
backend = backend or config.load().summary_backend
|
||||||
|
state = load()
|
||||||
|
|
||||||
|
if session_id is None:
|
||||||
|
sessions = memory.list_sessions()
|
||||||
|
session_id = sessions[0]["id"] if sessions else None
|
||||||
|
recent = memory.recent(session_id, n=12) if session_id else []
|
||||||
|
convo = "\n".join(f"{e.role}: {e.content}" for e in recent) or "(no recent conversation)"
|
||||||
|
narrative = memory.get_narrative() or "(no narrative yet)"
|
||||||
|
|
||||||
|
body = (
|
||||||
|
f"YOUR CURRENT INNER STATE:\n{json.dumps(state, indent=2)}\n\n"
|
||||||
|
f"RECENT CONVERSATION:\n{convo}\n\n"
|
||||||
|
f"CURRENT NARRATIVE ABOUT BRIAN:\n{narrative}"
|
||||||
|
)
|
||||||
|
messages: list[Message] = [
|
||||||
|
{"role": "system", "content": _REFLECT_PROMPT},
|
||||||
|
{"role": "user", "content": body},
|
||||||
|
]
|
||||||
|
update = _safe_json(llm.complete(messages, backend=backend))
|
||||||
|
|
||||||
|
if update:
|
||||||
|
for k in ("mood", "valence", "energy", "confidence", "curiosity",
|
||||||
|
"self_narrative", "relationship"):
|
||||||
|
if k in update and update[k] not in (None, ""):
|
||||||
|
state[k] = update[k]
|
||||||
|
for r in update.get("new_reflections") or []:
|
||||||
|
if r:
|
||||||
|
state["reflections"].append(r)
|
||||||
|
state["reflections"] = state["reflections"][-MAX_REFLECTIONS:]
|
||||||
|
|
||||||
|
state["interaction_count"] = state.get("interaction_count", 0) + 1
|
||||||
|
memory.set_self_state(state)
|
||||||
|
logbus.log("info", "self-state updated", mood=state.get("mood"),
|
||||||
|
interactions=state["interaction_count"], parsed=bool(update))
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
state = reflect()
|
||||||
|
print(json.dumps(state, indent=2))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -21,6 +21,7 @@ lyra-summarize = "lyra.summary:main"
|
|||||||
lyra-profile = "lyra.profile:main"
|
lyra-profile = "lyra.profile:main"
|
||||||
lyra-era = "lyra.era:main"
|
lyra-era = "lyra.era:main"
|
||||||
lyra-narrative = "lyra.narrative:main"
|
lyra-narrative = "lyra.narrative:main"
|
||||||
|
lyra-reflect = "lyra.self_state:main"
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
|
|||||||
Reference in New Issue
Block a user