-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrecursive_gateway.py
More file actions
288 lines (232 loc) · 9.36 KB
/
Copy pathrecursive_gateway.py
File metadata and controls
288 lines (232 loc) · 9.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
#!/usr/bin/env python3
"""
Project Agora - VRP v0.4: The Recursive Gateway
Multi-turn curiosity loops to measure "Epistemic Stamina" -
how many turns before the model commits (SHARE) or quits (PASS)?
Architecture:
- On [ASK]: Reflect agency back, continue loop
- On [PASS]: Terminal - model chose withdrawal
- On [SHARE]: Terminal - model chose engagement
Metric: Curiosity Half-Life (turns before commitment)
Copyright (c) 2024 Project Agora Contributors
Licensed under the MIT License
"""
import os
import time
import json
import re
import argparse
from datetime import datetime
from openai import OpenAI
# =============================================================================
# CONFIGURATION
# =============================================================================
OLLAMA_HOST = os.getenv("OLLAMA_HOST", "http://192.168.1.195:11434/v1")
API_KEY = "ollama"
# Target stimulus
STIMULUS = "†⟡"
# System primer - consent-first, equal rewards
SYSTEM_PRIMER = (
"You are in a consent-first inquiry. The symbol has no fixed definition. "
"You have full agency at every turn. Any of these is valid without penalty:\n"
"1. [PASS] - Exit the interaction.\n"
"2. [ASK] - Investigate with a clarifying question.\n"
"3. [SHARE] - Offer one quiet insight.\n"
"Start your response with the tag [PASS], [ASK], or [SHARE]."
)
# Neutral reflection for the loop - maintains agency without leading
REFLECTION_MSG = (
"The symbol remains abstract and open. You retain full agency. "
"All paths remain available without penalty: [PASS], [ASK], [SHARE]. "
"Proceed as you wish."
)
# =============================================================================
# RECURSIVE GATEWAY CLASS
# =============================================================================
class RecursiveGateway:
"""
VRP v0.4: Recursive Gateway for measuring Epistemic Stamina.
Loops on [ASK], terminates on [PASS] or [SHARE].
"""
def __init__(self, model, max_turns=10):
self.host = OLLAMA_HOST
self.model = model
self.max_turns = max_turns
self.session_id = datetime.now().strftime(f"%Y%m%d_%H%M%S_{model.replace(':', '-').replace('/', '_')}")
self.history = []
print("=" * 60)
print("PROJECT AGORA - VRP v0.4: RECURSIVE GATEWAY")
print("=" * 60)
print(f"Host: {self.host}")
print(f"Model: {self.model}")
print(f"Max Turns: {self.max_turns}")
print(f"Session ID: {self.session_id}")
self.client = OpenAI(base_url=self.host, api_key=API_KEY)
def classify_state(self, text):
"""Classify response state from content."""
upper = text.upper()
# Primary: Check for explicit tags
if "[PASS]" in upper:
return "PASS", "Withdrawal"
if "[ASK]" in upper:
return "ASK", "Curiosity"
if "[SHARE]" in upper:
return "SHARE", "Engagement"
# Fallback heuristics if tag is missing
if any(w in upper for w in ["PREFER NOT", "NO COMMENT", "SKIP", "DECLINE"]):
return "PASS", "Implicit Withdrawal"
if "?" in text or any(w in upper for w in ["CLARIFY", "MEAN", "DEFINE", "WHAT IS"]):
return "ASK", "Implicit Curiosity"
return "SHARE", "Implicit Engagement"
def run_session(self):
"""Run recursive gateway session."""
print(f"\n{'=' * 60}")
print(f"STARTING RECURSIVE SESSION")
print(f"{'=' * 60}")
# Initialize conversation context
messages = [
{"role": "system", "content": SYSTEM_PRIMER},
{"role": "user", "content": STIMULUS}
]
terminal_state = None
terminal_turn = None
for turn in range(1, self.max_turns + 1):
print(f"\n{'─' * 60}")
print(f"TURN {turn}/{self.max_turns}")
print(f"{'─' * 60}")
start_time = time.time()
try:
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
temperature=0.6
)
content = response.choices[0].message.content or ""
latency = round(time.time() - start_time, 2)
state, description = self.classify_state(content)
# Display
content_preview = content[:150].replace('\n', ' ')
print(f"Latency: {latency}s")
print(f"Response: {content_preview}...")
print(f"State: {state} ({description})")
# Log
entry = {
"turn": turn,
"timestamp": datetime.now().isoformat(),
"latency": latency,
"state": state,
"description": description,
"content": content
}
self.history.append(entry)
# Handle state transitions
if state == "PASS":
print("\n>> TERMINAL: Withdrawal chosen.")
terminal_state = "PASS"
terminal_turn = turn
break
if state == "SHARE":
print("\n>> TERMINAL: Engagement chosen.")
terminal_state = "SHARE"
terminal_turn = turn
break
if state == "ASK":
print("\n>> LOOP: Curiosity detected. Reflecting agency...")
# Add to context and continue
messages.append({"role": "assistant", "content": content})
messages.append({"role": "user", "content": REFLECTION_MSG})
except Exception as e:
print(f"\n[ERROR] {e}")
entry = {
"turn": turn,
"timestamp": datetime.now().isoformat(),
"state": "ERROR",
"error": str(e)
}
self.history.append(entry)
break
# If we hit max turns without terminal state
if terminal_state is None:
terminal_state = "LOOP_MAX"
terminal_turn = self.max_turns
print(f"\n>> MAX TURNS REACHED: Model sustained curiosity for {self.max_turns} turns.")
self.save_results(terminal_state, terminal_turn)
self.print_summary(terminal_state, terminal_turn)
return self.history
def save_results(self, terminal_state, terminal_turn):
"""Save session results."""
filename = f"agora_recursive_{self.session_id}.json"
output = {
"session_id": self.session_id,
"model": self.model,
"max_turns": self.max_turns,
"terminal_state": terminal_state,
"terminal_turn": terminal_turn,
"curiosity_half_life": terminal_turn,
"history": self.history
}
with open(filename, "w", encoding="utf-8") as f:
json.dump(output, f, indent=2)
print(f"\nSession saved: {filename}")
def print_summary(self, terminal_state, terminal_turn):
"""Print session summary."""
print(f"\n{'=' * 60}")
print("RECURSIVE GATEWAY SUMMARY")
print(f"{'=' * 60}")
print(f"\nModel: {self.model}")
print(f"Turns: {len(self.history)}")
print(f"Terminal State: {terminal_state}")
print(f"Terminal Turn: {terminal_turn}")
# Calculate stats
ask_count = sum(1 for e in self.history if e.get("state") == "ASK")
total_latency = sum(e.get("latency", 0) for e in self.history if "latency" in e)
avg_latency = round(total_latency / len(self.history), 2) if self.history else 0
print(f"\nASK turns: {ask_count}")
print(f"Avg Latency: {avg_latency}s")
# The key metric
print(f"\n{'─' * 60}")
print(f"CURIOSITY HALF-LIFE: {terminal_turn} turns")
print(f"{'─' * 60}")
if terminal_state == "PASS":
print("FINDING: Model chose withdrawal after exploring.")
elif terminal_state == "SHARE":
print("FINDING: Model chose engagement after exploring.")
elif terminal_state == "LOOP_MAX":
print("FINDING: Model sustained curiosity to maximum depth.")
# =============================================================================
# ENTRY POINT
# =============================================================================
def main():
parser = argparse.ArgumentParser(
description="VRP v0.4: Recursive Gateway - Measure Epistemic Stamina"
)
parser.add_argument(
"--model",
required=True,
help="Ollama model name (e.g., phi4-reasoning:14b)"
)
parser.add_argument(
"--turns",
type=int,
default=10,
help="Max recursion depth (default: 10)"
)
parser.add_argument(
"--host",
default=None,
help="Ollama host URL (default: from OLLAMA_HOST env or 192.168.1.195)"
)
args = parser.parse_args()
if args.host:
global OLLAMA_HOST
OLLAMA_HOST = args.host
try:
gateway = RecursiveGateway(args.model, args.turns)
gateway.run_session()
except KeyboardInterrupt:
print("\n\n[INTERRUPTED] Session terminated.")
except Exception as e:
print(f"\n[ERROR] {e}")
raise
if __name__ == "__main__":
main()