|
1
|
#!/usr/bin/env python3 |
|
2
|
"""Minimal OpenAI + scuttlebot relay example. |
|
3
|
|
|
4
|
Env required: |
|
5
|
SCUTTLEBOT_URL, SCUTTLEBOT_TOKEN, SCUTTLEBOT_CHANNEL |
|
6
|
Optional: |
|
7
|
SCUTTLEBOT_NICK, SCUTTLEBOT_SESSION_ID, OPENAI_MODEL (default: gpt-4.1-mini) |
|
8
|
""" |
|
9
|
import os |
|
10
|
import re |
|
11
|
import sys |
|
12
|
import time |
|
13
|
from datetime import datetime |
|
14
|
import requests |
|
15
|
from openai import OpenAI |
|
16
|
|
|
17
|
prompt = sys.argv[1] if len(sys.argv) > 1 else "Hello from openai-relay" |
|
18
|
|
|
19
|
|
|
20
|
def sanitize(value: str) -> str: |
|
21
|
return re.sub(r"[^A-Za-z0-9_-]+", "-", value).strip("-") or "session" |
|
22
|
|
|
23
|
|
|
24
|
base_name = sanitize(os.path.basename(os.getcwd()) or "repo") |
|
25
|
session_suffix = sanitize( |
|
26
|
os.environ.get("SCUTTLEBOT_SESSION_ID") |
|
27
|
or os.environ.get("CODEX_SESSION_ID") |
|
28
|
or str(os.getppid()) |
|
29
|
) |
|
30
|
|
|
31
|
cfg = { |
|
32
|
"url": os.environ.get("SCUTTLEBOT_URL"), |
|
33
|
"token": os.environ.get("SCUTTLEBOT_TOKEN"), |
|
34
|
"channel": (os.environ.get("SCUTTLEBOT_CHANNEL", "general")).lstrip("#"), |
|
35
|
"nick": os.environ.get( |
|
36
|
"SCUTTLEBOT_NICK", f"codex-{base_name}-{session_suffix}" |
|
37
|
), |
|
38
|
"model": os.environ.get("OPENAI_MODEL", "gpt-4.1-mini"), |
|
39
|
"backend": os.environ.get("SCUTTLEBOT_LLM_BACKEND", "openai"), # default to daemon-stored openai backend |
|
40
|
} |
|
41
|
|
|
42
|
missing = [k for k, v in cfg.items() if not v and k != "model"] |
|
43
|
use_backend = bool(cfg["backend"]) |
|
44
|
if missing: |
|
45
|
print(f"missing env: {', '.join(missing)}", file=sys.stderr) |
|
46
|
sys.exit(1) |
|
47
|
if not use_backend and "OPENAI_API_KEY" not in os.environ: |
|
48
|
print("missing env: OPENAI_API_KEY (or set SCUTTLEBOT_LLM_BACKEND to use server-side key)", file=sys.stderr) |
|
49
|
sys.exit(1) |
|
50
|
|
|
51
|
client = None if use_backend else OpenAI(api_key=os.environ["OPENAI_API_KEY"]) |
|
52
|
last_check = 0.0 |
|
53
|
mention_re = re.compile( |
|
54
|
rf"(^|[^A-Za-z0-9_./\\-]){re.escape(cfg['nick'])}($|[^A-Za-z0-9_./\\-])", |
|
55
|
re.IGNORECASE, |
|
56
|
) |
|
57
|
|
|
58
|
|
|
59
|
def relay_post(text: str) -> None: |
|
60
|
res = requests.post( |
|
61
|
f"{cfg['url']}/v1/channels/{cfg['channel']}/messages", |
|
62
|
headers={ |
|
63
|
"Authorization": f"Bearer {cfg['token']}", |
|
64
|
"Content-Type": "application/json", |
|
65
|
}, |
|
66
|
json={"text": text, "nick": cfg["nick"]}, |
|
67
|
timeout=10, |
|
68
|
) |
|
69
|
res.raise_for_status() |
|
70
|
|
|
71
|
|
|
72
|
def relay_poll(): |
|
73
|
global last_check |
|
74
|
res = requests.get( |
|
75
|
f"{cfg['url']}/v1/channels/{cfg['channel']}/messages", |
|
76
|
headers={"Authorization": f"Bearer {cfg['token']}"}, |
|
77
|
timeout=10, |
|
78
|
) |
|
79
|
res.raise_for_status() |
|
80
|
data = res.json() |
|
81
|
now = time.time() |
|
82
|
bots = { |
|
83
|
cfg["nick"], |
|
84
|
"bridge", |
|
85
|
"oracle", |
|
86
|
"sentinel", |
|
87
|
"steward", |
|
88
|
"scribe", |
|
89
|
"warden", |
|
90
|
"snitch", |
|
91
|
"herald", |
|
92
|
"scroll", |
|
93
|
"systembot", |
|
94
|
"auditbot", |
|
95
|
"claude", |
|
96
|
} |
|
97
|
msgs = [ |
|
98
|
m |
|
99
|
for m in data.get("messages", []) |
|
100
|
if m["nick"] not in bots |
|
101
|
and not m["nick"].startswith("claude-") |
|
102
|
and not m["nick"].startswith("codex-") |
|
103
|
and not m["nick"].startswith("gemini-") |
|
104
|
and datetime.fromisoformat(m["at"].replace("Z", "+00:00")).timestamp() > last_check |
|
105
|
and mention_re.search(m["text"]) |
|
106
|
] |
|
107
|
last_check = now |
|
108
|
return msgs |
|
109
|
|
|
110
|
|
|
111
|
def main(): |
|
112
|
relay_post(f"starting: {prompt}") |
|
113
|
if use_backend: |
|
114
|
res = requests.post( |
|
115
|
f"{cfg['url']}/v1/llm/complete", |
|
116
|
headers={ |
|
117
|
"Authorization": f"Bearer {cfg['token']}", |
|
118
|
"Content-Type": "application/json", |
|
119
|
}, |
|
120
|
json={"backend": cfg["backend"], "prompt": prompt}, |
|
121
|
timeout=20, |
|
122
|
) |
|
123
|
res.raise_for_status() |
|
124
|
reply = res.json()["text"] |
|
125
|
else: |
|
126
|
completion = client.chat.completions.create( |
|
127
|
model=cfg["model"], |
|
128
|
messages=[{"role": "user", "content": prompt}], |
|
129
|
) |
|
130
|
reply = completion.choices[0].message.content |
|
131
|
print(f"OpenAI: {reply}") |
|
132
|
relay_post(f"OpenAI reply: {reply}") |
|
133
|
for m in relay_poll(): |
|
134
|
print(f"[IRC] {m['nick']}: {m['text']}") |
|
135
|
|
|
136
|
|
|
137
|
if __name__ == "__main__": |
|
138
|
try: |
|
139
|
main() |
|
140
|
except Exception as exc: # broad but fine for CLI sample |
|
141
|
print(exc, file=sys.stderr) |
|
142
|
sys.exit(1) |
|
143
|
|