Skip to content

Commit 319543f

Browse files
committed
Go
1 parent 9570869 commit 319543f

3 files changed

Lines changed: 188 additions & 8 deletions

File tree

.github-minimum-intelligence/lifecycle/local-chat.ts

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ const piSettingsPath = resolve(minimumIntelligenceDir, ".pi", "settings.json");
112112
const memoryLogPath = resolve(minimumIntelligenceDir, "memory.log");
113113
const lastRunRawPath = resolve(stateDir, "local-last-run.jsonl");
114114

115+
// Dedicated pi agent dir used only in local mode. Pointing PI_CODING_AGENT_DIR
116+
// here makes pi read our generated models.json (a custom OpenAI-compatible
117+
// provider) without disturbing the user's global ~/.pi/agent config.
118+
const localAgentDir = resolve(stateDir, "pi-agent");
119+
const localModelsPath = resolve(localAgentDir, "models.json");
120+
115121
// Repo-root-relative session dir, matching agent.ts.
116122
const sessionsDirRelative = ".github-minimum-intelligence/state/sessions";
117123

@@ -146,12 +152,11 @@ const LOCAL_BRAND_DEFAULTS: Record<string, { label: string; baseUrl: string }> =
146152
vllm: { label: "vLLM", baseUrl: "http://localhost:8000/v1" },
147153
};
148154

149-
// Brand labels that map to a "openai-compatible" pi invocation. The key is
150-
// what the user types or configures; the value is what pi actually receives
151-
// (always "openai" today because pi has no first-class lmstudio/ollama/vllm
152-
// provider — they all speak OpenAI Chat Completions).
155+
// Local brands (lmstudio/ollama/vllm) are registered as first-class custom
156+
// providers in a generated models.json (see ensureLocalProviderConfig), so pi
157+
// receives the brand name verbatim and reaches the local server over the
158+
// OpenAI Chat Completions API. The brand IS the pi provider name now.
153159
function resolvePiProvider(userProvider: string): string {
154-
if (LOCAL_PROVIDERS.has(userProvider)) return "openai";
155160
return userProvider;
156161
}
157162

@@ -740,6 +745,59 @@ function isLocalProvider(provider: string): boolean {
740745
return false;
741746
}
742747

748+
/**
749+
* Resolve the effective OpenAI-compatible base URL for a local provider,
750+
* honouring explicit env vars first, then well-known brand defaults.
751+
*/
752+
function resolveLocalBaseUrl(provider: string): string {
753+
return (
754+
process.env.LOCAL_LLM_BASE_URL ||
755+
process.env.OPENAI_BASE_URL ||
756+
LOCAL_BRAND_DEFAULTS[provider]?.baseUrl ||
757+
"http://localhost:1234/v1"
758+
);
759+
}
760+
761+
/**
762+
* Configure pi to talk to a local OpenAI-compatible server (LM Studio, Ollama,
763+
* vLLM, or an `openai` provider pointed at LOCAL_LLM_BASE_URL).
764+
*
765+
* Why this exists: pi's built-in `openai` provider ignores OPENAI_BASE_URL and
766+
* defaults to the Responses API, so it would contact the real api.openai.com
767+
* and fail with a 401. The supported mechanism for a local server is a
768+
* `models.json` describing a custom provider with an explicit `baseUrl` and the
769+
* `openai-completions` API. pi only reads `models.json` from its agent dir, so
770+
* we point PI_CODING_AGENT_DIR at a repo-local directory and write the file
771+
* there. This leaves the user's global ~/.pi/agent untouched and is applied
772+
* only for local providers.
773+
*
774+
* `compat.supportsDeveloperRole` / `supportsReasoningEffort` are disabled
775+
* because many local servers reject the `developer` role and the
776+
* `reasoning_effort` parameter used by reasoning-capable cloud models.
777+
*/
778+
function ensureLocalProviderConfig(provider: string, model: string): void {
779+
const baseUrl = resolveLocalBaseUrl(provider);
780+
mkdirSync(localAgentDir, { recursive: true });
781+
const modelsConfig = {
782+
providers: {
783+
[provider]: {
784+
baseUrl,
785+
api: "openai-completions",
786+
apiKey: "local",
787+
compat: {
788+
supportsDeveloperRole: false,
789+
supportsReasoningEffort: false,
790+
},
791+
models: [{ id: model }],
792+
},
793+
},
794+
};
795+
writeFileSync(localModelsPath, JSON.stringify(modelsConfig, null, 2) + "\n");
796+
process.env.PI_CODING_AGENT_DIR = localAgentDir;
797+
process.env.OPENAI_BASE_URL = baseUrl;
798+
if (!process.env.OPENAI_API_KEY) process.env.OPENAI_API_KEY = "local";
799+
}
800+
743801
// ─── pi binary location ───────────────────────────────────────────────────────
744802

745803
function locatePiBin(): string {
@@ -831,11 +889,12 @@ async function runTurn(
831889
: null;
832890

833891
// Map brand providers (lmstudio/ollama/vllm) to what pi actually
834-
// understands today (openai-compatible Chat Completions). pi has no
835-
// first-class lmstudio provider, so the brand is purely a label for
836-
// the user; the wire-format is always openai-compatible.
892+
// understands. For local mode we (re)write models.json so pi reaches the
893+
// local OpenAI-compatible server via a custom provider of the same name;
894+
// doing it here also picks up runtime /model and /provider switches.
837895
const piProvider = resolvePiProvider(rt.provider);
838896
const localMode = isLocalProvider(rt.provider);
897+
if (localMode) ensureLocalProviderConfig(rt.provider, rt.model);
839898
const args: string[] = [
840899
"--mode", "json",
841900
"--tools", "read,bash,edit,write,grep,find,ls",
@@ -856,6 +915,10 @@ async function runTurn(
856915
try {
857916
const proc = Bun.spawn([rt.piBin, ...args], {
858917
cwd: repoRoot,
918+
// Pass env explicitly so runtime mutations (e.g. PI_CODING_AGENT_DIR
919+
// and OPENAI_BASE_URL set by ensureLocalProviderConfig) reliably reach
920+
// the pi child on every platform.
921+
env: { ...process.env },
859922
stdout: "pipe",
860923
stderr: "inherit",
861924
});
@@ -1811,6 +1874,11 @@ async function main(): Promise<void> {
18111874
piBin,
18121875
};
18131876

1877+
// For local providers, generate models.json and point PI_CODING_AGENT_DIR at
1878+
// it up front so the REPL banner shows the right endpoint and the first turn
1879+
// is correctly wired.
1880+
if (isLocalProvider(rt.provider)) ensureLocalProviderConfig(rt.provider, rt.model);
1881+
18141882
// One-shot mode.
18151883
if (args.prompt) {
18161884
try {

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ node_modules/
66
# Local-only chat threads created by lifecycle/local-chat.ts
77
.github-minimum-intelligence/state/threads/
88

9+
# Per-session transcripts written to state/sessions
10+
.github-minimum-intelligence/state/sessions/
11+
12+
# Generated pi agent dir for local mode (models.json, etc.)
13+
.github-minimum-intelligence/state/pi-agent/
14+
915
# Per-thread scratch / debug artefacts written by local-chat.ts
1016
.github-minimum-intelligence/state/local-last-run.jsonl
1117
.github-minimum-intelligence/memory.log

chat_agent.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# chat_agent.py
2+
import datetime
3+
from collections import deque
4+
5+
class ContextualAgent:
6+
"""
7+
A sophisticated class designed to simulate a highly contextual AI chat agent.
8+
It models deep understanding by managing explicit memory, short-term context,
9+
and external knowledge retrieval.
10+
"""
11+
12+
def __init__(self, max_context_size=10):
13+
# --- Memory Components ---
14+
# 1. Long-Term Memory (LTM): Permanent facts learned over time.
15+
self.ltm = {} # Key: Topic/Concept, Value: Summary of insights
16+
17+
# 2. Short-Term Context (STM): Recent conversation history for immediate reference.
18+
# Using deque to automatically manage the size (sliding window).
19+
self.stm = deque(maxlen=max_context_size)
20+
21+
print("Agent Initialized: Core context buffers established.")
22+
23+
def log_interaction(self, user_input: str, agent_response: str):
24+
"""Records the current exchange in Short-Term Memory (STM)."""
25+
log = f"[User]: {user_input} | [Agent]: {agent_response}"
26+
self.stm.append((datetime.datetime.now(), log))
27+
28+
def retrieve_memory(self, query: str) -> str:
29+
"""Simulates complex knowledge retrieval across LTM and STM."""
30+
retrieval = []
31+
32+
# Check STM first (immediate relevance)
33+
for timestamp, log in list(self.stm):
34+
if query.lower() in log.lower():
35+
retrieval.append(f"[RECENT CONTEXT]: {log}")
36+
37+
# Check LTM second (deep knowledge recall)
38+
# In a real system, this would involve vector databases and semantic search.
39+
for topic, summary in self.ltm.items():
40+
if query.lower() in topic.lower() or "memory" in query.lower(): # Simple trigger check for demo
41+
retrieval.append(f"[LONG-TERM KNOWLEDGE]: Regarding '{topic}', remember: {summary}")
42+
43+
if not retrieval:
44+
return "\n[Memory Retrieval]: No specific memory matches the current context."
45+
else:
46+
return "\n" + "\n---\n".join(retrieval)
47+
48+
49+
def process_request(self, user_input: str):
50+
"""
51+
The core logic flow for generating a contextual response.
52+
This is where 'intelligence' resides.
53+
"""
54+
print("\n--- Thinking Process Initiated ---")
55+
# Step 1: Retrieve relevant memory before responding (Contextual grounding)
56+
memory = self.retrieve_memory(user_input)
57+
print("🔍 Memory Check Complete:", memory if "Memory" in memory else "None found.")
58+
59+
# Step 2: Hypothetical Knowledge Lookup / External Tool Use
60+
# In a real system, this would call APIs (e.g., Google Search, Code Interpreter).
61+
print("[Tool]: Querying external knowledge bases for deep context...")
62+
knowledge_retrieved = "\n[TOOL OUTPUT]: Found related concepts in cosmology and philosophy."
63+
64+
# Step 3: Synthesis & Response Generation
65+
final_response = f"""\n🧠 Agent Synthesis:\nBased on the user's input, we must synthesize the recent conversation ({memory.strip()}) with external knowledge ({knowledge_retrieved.strip()}). \n\n"The core of the answer lies not in retrieval, but in identifying the underlying pattern that connects these two disparate fields..."\n"""
66+
67+
# Step 4: Update Memory (Learning)
68+
self._learn(user_input, final_response)
69+
70+
return final_response
71+
72+
def _learn(self, user_input: str, response: str):
73+
"""Updates Long-Term Memory based on successful interactions."""
74+
# Simple learning heuristic: if the conversation touches upon a key concept (like 'blue' or 'memory'), store it.
75+
if "blue" in user_input.lower() and "deep" in response.lower():
76+
self.ltm['The Blue Sky'] = "The sky is best analyzed as a metaphysical placeholder for human potentiality, reflecting cosmic indifference."
77+
elif "memory" in user_input.lower():
78+
self.ltm['The Nature of Memory'] = "Memory is not a recording; it is an active reconstruction process prone to bias and emotional filtering."
79+
80+
# --- DEMONSTRATION ---
81+
82+
if __name__ == "__main__":
83+
agent = ContextualAgent(max_context_size=5)
84+
85+
print("\n=========================================")
86+
print("--- Simulation: Initial Query (Memory Check) ---")
87+
user1 = "What are the deepest philosophical implications of constant change?"
88+
response1 = agent.process_request(user1)
89+
print(f"\n[Final Agent Output]: {response1}")
90+
agent.log_interaction(user1, response1)
91+
92+
print("\n=========================================")
93+
print("--- Simulation: Second Query (Contextual Follow-up) ---")
94+
# The agent should remember 'change' and apply it to a new topic like 'memory'.
95+
user2 = "How does the instability of memory relate to constant change?"
96+
response2 = agent.process_request(user2)
97+
print(f"\n[Final Agent Output]: {response2}")
98+
agent.log_interaction(user2, response2)
99+
100+
print("\n=========================================")
101+
print("--- Simulation: New Topic (Testing LTM Recall) ---")
102+
# The agent should now recall the 'blue sky' lesson when we discuss deep topics again.
103+
user3 = "I feel overwhelmed by vastness; like looking up at a huge blue expanse."
104+
response3 = agent.process_request(user3)
105+
print(f"\n[Final Agent Output]: {response3}")
106+

0 commit comments

Comments
 (0)