igardner commited on
Commit
22b6988
Β·
1 Parent(s): d9b564d
Files changed (1) hide show
  1. app.py +213 -253
app.py CHANGED
@@ -1,365 +1,325 @@
1
- # === IMPORTS: Standard, secure, minimal ===
2
  import gradio as gr
3
  import json
4
  import re
5
  from datetime import datetime
6
  from contextlib import contextmanager
7
- from typing import Dict, Any, Optional
8
 
9
- # SQLAlchemy for robust, transactional data persistence
10
  from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, ForeignKey, JSON
11
  from sqlalchemy.ext.declarative import declarative_base
12
  from sqlalchemy.orm import sessionmaker, relationship
13
-
14
- # Discord client (unofficial but functional; replace with discord.py if preferred)
15
  import discum
 
16
 
17
- # smolagents for agent-tool orchestration (correctly integrated)
18
- from smolagents import Tool, Agent
19
-
20
-
21
- # === DATABASE CONFIGURATION: Persistent, local, atomic ===
22
- # Use SQLite for simplicity; replace with PostgreSQL/MySQL in production
23
- DB_URL = "sqlite:///puppets.db"
24
  Base = declarative_base()
25
- engine = create_engine(DB_URL, connect_args={"check_same_thread": False}) # Safe for Gradio
26
  SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
27
 
28
-
29
- # === DATA MODELS: Immutable schema with validation ===
30
  class Personality(Base):
31
- """
32
- Represents a reusable AI personality.
33
- - `base_prompt`: Core behavior definition (e.g., "You are a sarcastic chef...")
34
- - `fine_tune_config`: LLM parameters (model, temperature) as JSON
35
- """
36
  __tablename__ = "personalities"
37
  id = Column(Integer, primary_key=True, autoincrement=True)
38
- name = Column(String(255), nullable=False, unique=False) # Allow duplicate names
39
  base_prompt = Column(Text, nullable=False)
40
  fine_tune_config = Column(JSON, default=dict, nullable=False)
41
  created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
42
 
43
-
44
  class ChannelConfig(Base):
45
- """
46
- Binds a personality to a Discord channel with optional overrides.
47
- - `channel_id`: Discord channel snowflake (string to avoid overflow)
48
- - `channel_instructions`: Channel-specific rules (e.g., "Always use emojis")
49
- """
50
  __tablename__ = "channel_configs"
51
  id = Column(Integer, primary_key=True, autoincrement=True)
52
- channel_id = Column(String(64), nullable=False) # Discord IDs are up to 19 digits
53
  personality_id = Column(Integer, ForeignKey("personalities.id"), nullable=False)
54
  channel_instructions = Column(Text, nullable=True)
55
  created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
56
- # Relationship for easy access to personality data
57
  personality = relationship("Personality", lazy="joined")
58
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
- # === INITIALIZE DATABASE: Idempotent, safe on startup ===
61
- # Creates tables if they don't exist; no-op if they do
62
  Base.metadata.create_all(bind=engine)
63
 
64
-
65
- # === GLOBAL STATE: Isolated, managed puppet instances ===
66
- # Maps integer puppet_id β†’ live discum.Client
67
- # ⚠️ In production, add TTL cleanup or explicit shutdown hooks
68
  _discum_clients: Dict[int, discum.Client] = {}
69
 
70
-
71
- # === DATABASE SESSION MANAGER: Transaction-safe context ===
72
  @contextmanager
73
  def get_db_session():
74
- """
75
- Provides a transactional database session.
76
- - Commits on success
77
- - Rolls back on exception
78
- - Always closes session (prevents leaks)
79
- Usage: `with get_db_session() as session: ...`
80
- """
81
  session = SessionLocal()
82
  try:
83
  yield session
 
84
  except Exception:
85
  session.rollback()
86
  raise
87
  finally:
88
  session.close()
89
 
90
-
91
- # === VALIDATION HELPERS: Prevent invalid inputs ===
92
  def validate_discord_id(discord_id: str) -> bool:
93
- """
94
- Validates Discord snowflake IDs.
95
- - Must be a string of digits (1-20 characters)
96
- - Rejects empty, non-numeric, or malformed IDs
97
- """
98
- if not isinstance(discord_id, str) or not discord_id:
99
- return False
100
- return bool(re.fullmatch(r"^\d{1,20}$", discord_id))
101
-
102
 
103
- # === TOOL DEFINITIONS: smolagents-compatible, structured I/O ===
104
- # Each tool:
105
- # - Inherits from smolagents.Tool
106
- # - Defines strict inputs/outputs
107
- # - Returns JSON (not raw strings) for agent consumption
108
- # - Handles errors gracefully (no crashes)
109
 
 
110
  class ListPersonalitiesTool(Tool):
111
- """Tool for agent to discover available personalities."""
112
  name = "list_personalities"
113
- description = "Retrieve all defined personalities with IDs and names."
114
- inputs = {} # No inputs required
115
- output_type = "string" # smolagents requires string; we serialize JSON
116
-
117
- def forward(self) -> str:
118
- """Returns JSON array of personalities."""
119
  try:
120
- with get_db_session() as session:
121
- personalities = session.query(Personality).all()
122
- result = [
123
- {"id": p.id, "name": p.name}
124
- for p in personalities
125
- ]
126
- return json.dumps(result)
127
  except Exception as e:
128
- # Never crash the agent; return structured error
129
- return json.dumps({"error": "db_query_failed", "message": str(e)})
130
-
131
 
132
  class GetChannelPromptTool(Tool):
133
- """Tool to build the final prompt for a channel using personality + overrides."""
134
  name = "get_channel_prompt"
135
- description = "Generate the full prompt for a channel by combining personality and channel instructions."
136
  inputs = {
137
- "channel_id": {"type": "string", "description": "Discord channel ID"},
138
- "personality_id": {"type": "integer", "description": "Personality ID"},
139
  }
140
  output_type = "string"
141
-
142
- def forward(self, channel_id: str, personality_id: int) -> str:
143
- """Returns JSON with prompt and LLM config."""
144
- # Validate inputs early
145
- if not validate_discord_id(channel_id):
146
- return json.dumps({"error": "invalid_channel_id"})
147
- if personality_id <= 0:
148
- return json.dumps({"error": "invalid_personality_id"})
149
-
150
  try:
151
- with get_db_session() as session:
152
- # Fetch personality
153
- personality = session.query(Personality).filter(
154
- Personality.id == personality_id
155
- ).first()
156
- if not personality:
157
- return json.dumps({"error": "personality_not_found"})
158
-
159
- # Fetch channel config (if exists)
160
- config = session.query(ChannelConfig).filter(
161
- ChannelConfig.channel_id == channel_id,
162
- ChannelConfig.personality_id == personality_id
163
  ).first()
164
-
165
- # Build final prompt: base + channel instructions
166
- prompt_parts = [personality.base_prompt]
167
- if config and config.channel_instructions:
168
- prompt_parts.append(f"\n\n[CHANNEL RULES]\n{config.channel_instructions}")
169
- final_prompt = "\n".join(prompt_parts)
170
-
171
  return json.dumps({
172
- "prompt": final_prompt,
173
- "fine_tune_config": personality.fine_tune_config or {}
174
  })
175
  except Exception as e:
176
- return json.dumps({"error": "prompt_generation_failed", "message": str(e)})
177
-
178
 
179
  class InitializePuppetTool(Tool):
180
- """Tool to spawn a new Discord puppet (client) with a token."""
181
  name = "initialize_puppet"
182
- description = "Initialize a Discord client (puppet) with a token."
183
  inputs = {
184
- "puppet_id": {"type": "integer", "description": "Unique ID for this puppet (e.g., 1)"},
185
- "token": {"type": "string", "description": "Discord account or bot token"},
186
  }
187
  output_type = "string"
188
-
189
- def forward(self, puppet_id: int, token: str) -> str:
190
- """Returns success status or error."""
191
- if puppet_id <= 0:
192
- return json.dumps({"error": "puppet_id_must_be_positive"})
193
- if not isinstance(token, str) or len(token) < 50:
194
- return json.dumps({"error": "invalid_token_format"})
195
-
196
  try:
197
- # Clean up existing puppet if present
198
  if puppet_id in _discum_clients:
199
- try:
200
- _discum_clients[puppet_id].close()
201
- except:
202
- pass # Ignore cleanup errors
203
-
204
- # Initialize new client
205
- client = discum.Client(token=token, log=False)
206
- _discum_clients[puppet_id] = client
207
-
208
  return json.dumps({"status": "success", "puppet_id": puppet_id})
209
  except Exception as e:
210
- return json.dumps({"error": "puppet_initialization_failed", "message": str(e)})
211
-
212
 
213
  class ConfigureChannelTool(Tool):
214
- """Tool to bind a personality to a channel for a specific puppet."""
215
  name = "configure_channel"
216
- description = "Assign a personality to a Discord channel with optional instructions."
217
  inputs = {
218
- "puppet_id": {"type": "integer", "description": "Puppet ID (must be initialized)"},
219
- "channel_id": {"type": "string", "description": "Discord channel ID"},
220
- "personality_id": {"type": "integer", "description": "Personality ID"},
221
- "instructions": {"type": "string", "description": "Channel-specific instructions (optional)"},
222
  }
223
  output_type = "string"
224
-
225
- def forward(self, puppet_id: int, channel_id: str, personality_id: int, instructions: str = "") -> str:
226
- """Returns success status or error."""
227
- # Validate inputs
228
- if puppet_id <= 0:
229
- return json.dumps({"error": "invalid_puppet_id"})
230
- if not validate_discord_id(channel_id):
231
- return json.dumps({"error": "invalid_channel_id"})
232
- if personality_id <= 0:
233
- return json.dumps({"error": "invalid_personality_id"})
234
-
235
- # Verify puppet is alive
236
- if puppet_id not in _discum_clients:
237
- return json.dumps({"error": "puppet_not_initialized"})
238
-
239
  try:
240
- with get_db_session() as session:
241
- # Verify personality exists
242
- personality = session.query(Personality).filter(
243
- Personality.id == personality_id
244
- ).first()
245
- if not personality:
246
- return json.dumps({"error": "personality_not_found"})
247
-
248
- # Save channel config
249
- config = ChannelConfig(
250
  channel_id=channel_id,
251
  personality_id=personality_id,
252
  channel_instructions=instructions.strip() or None
253
  )
254
- session.add(config)
255
-
256
  return json.dumps({"status": "success"})
257
  except Exception as e:
258
- return json.dumps({"error": "configuration_failed", "message": str(e)})
259
-
260
 
261
  class SendMessageTool(Tool):
262
- """Tool for a puppet to send a message to a channel."""
263
  name = "send_message"
264
- description = "Send a message to a Discord channel as a puppet."
265
  inputs = {
266
- "puppet_id": {"type": "integer", "description": "Puppet ID"},
267
- "channel_id": {"type": "string", "description": "Discord channel ID"},
268
- "message": {"type": "string", "description": "Message content"},
269
  }
270
  output_type = "string"
271
-
272
- def forward(self, puppet_id: int, channel_id: str, message: str) -> str:
273
- """Returns success status or error."""
274
- if puppet_id <= 0:
275
- return json.dumps({"error": "invalid_puppet_id"})
276
- if not validate_discord_id(channel_id):
277
- return json.dumps({"error": "invalid_channel_id"})
278
- if not message or not message.strip():
279
- return json.dumps({"error": "empty_message"})
280
-
281
- # Verify puppet is alive
282
- if puppet_id not in _discum_clients:
283
- return json.dumps({"error": "puppet_not_initialized"})
284
-
285
  try:
286
- client = _discum_clients[puppet_id]
287
- client.sendMessage(channel_id, message.strip())
288
  return json.dumps({"status": "success"})
289
  except Exception as e:
290
- return json.dumps({"error": "message_send_failed", "message": str(e)})
291
-
292
 
293
- # === TOOL REGISTRY: All tools available to the agent ===
294
  TOOLS = [
295
  ListPersonalitiesTool(),
296
  GetChannelPromptTool(),
297
  InitializePuppetTool(),
298
  ConfigureChannelTool(),
299
- SendMessageTool(),
300
  ]
301
 
302
-
303
- # === AGENT EXECUTOR: smolagents integration ===
304
- def execute_agent_goal(goal: str) -> str:
305
- """
306
- Runs the smolagents-powered agent with the user's goal.
307
- - Uses gpt-4 by default (change model as needed)
308
- - Returns raw agent output (string)
309
- - Safe: catches all exceptions
310
- """
 
 
311
  try:
312
- # Initialize agent with tools
313
- agent = Agent(tools=TOOLS, model="gpt-4") # Or your preferred model
314
- result = agent.run(goal)
315
- return str(result)
 
316
  except Exception as e:
317
- return f"Agent execution failed: {str(e)}"
318
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
 
320
- # === GRADIO INTERFACE: Clean, minimal, functional ===
321
- with gr.Blocks(title="Puppet Master") as demo:
322
- gr.Markdown("# 🎭 Puppet Master β€” smolagents-Powered Discord Orchestrator")
323
- gr.Markdown("Describe your goal. The agent will use tools to manage puppets, personalities, and channels.")
324
-
325
- # User input: natural language goal
326
- goal_input = gr.Textbox(
327
- label="Your Goal",
328
- placeholder="Example: Initialize puppet 1 with my token, assign personality 'Helper' to channel 123456789, and send 'Hello!'",
329
- lines=3
330
- )
331
-
332
- # Output: agent's actions and results
333
- output = gr.Textbox(label="Agent Output", lines=10, interactive=False)
334
 
335
- # Trigger agent execution
336
- gr.Button("Execute Goal").click(
337
- fn=execute_agent_goal,
338
- inputs=goal_input,
339
- outputs=output
340
- )
 
 
 
 
 
341
 
342
- # Documentation panel (hidden by default in prod; useful for dev)
343
- with gr.Accordion("Tool Documentation", open=False):
344
- gr.Markdown("""
345
- ### Available Tools
346
- - `list_personalities()`: Get all personalities
347
- - `get_channel_prompt(channel_id, personality_id)`: Build final prompt
348
- - `initialize_puppet(puppet_id, token)`: Spawn Discord client
349
- - `configure_channel(puppet_id, channel_id, personality_id, instructions)`: Bind personality to channel
350
- - `send_message(puppet_id, channel_id, message)`: Send message as puppet
351
- """)
352
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
 
354
- # === ENTRY POINT: Production-ready launch ===
355
  if __name__ == "__main__":
356
- # Launch with security best practices:
357
- # - Bind to localhost only (change to "0.0.0.0" for remote access)
358
- # - Disable sharing (public links are dangerous)
359
- # - Enable error display for debugging
360
- demo.launch(
361
- server_name="127.0.0.1", # Localhost only
362
- server_port=7860,
363
- show_error=True,
364
- share=False # Never enable in production
365
- )
 
1
+ # app.py
2
  import gradio as gr
3
  import json
4
  import re
5
  from datetime import datetime
6
  from contextlib import contextmanager
7
+ from typing import Dict, Any
8
 
 
9
  from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, ForeignKey, JSON
10
  from sqlalchemy.ext.declarative import declarative_base
11
  from sqlalchemy.orm import sessionmaker, relationship
 
 
12
  import discum
13
+ from smolagents import Tool
14
 
15
+ # === DATABASE SETUP ===
16
+ DB_PATH = "sqlite:///puppets.db"
 
 
 
 
 
17
  Base = declarative_base()
18
+ engine = create_engine(DB_PATH, connect_args={"check_same_thread": False})
19
  SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
20
 
21
+ # === MODELS ===
 
22
  class Personality(Base):
 
 
 
 
 
23
  __tablename__ = "personalities"
24
  id = Column(Integer, primary_key=True, autoincrement=True)
25
+ name = Column(String(255), nullable=False)
26
  base_prompt = Column(Text, nullable=False)
27
  fine_tune_config = Column(JSON, default=dict, nullable=False)
28
  created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
29
 
 
30
  class ChannelConfig(Base):
 
 
 
 
 
31
  __tablename__ = "channel_configs"
32
  id = Column(Integer, primary_key=True, autoincrement=True)
33
+ channel_id = Column(String(64), nullable=False)
34
  personality_id = Column(Integer, ForeignKey("personalities.id"), nullable=False)
35
  channel_instructions = Column(Text, nullable=True)
36
  created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
 
37
  personality = relationship("Personality", lazy="joined")
38
 
39
+ # === ENSURE SCHEMA COMPATIBILITY ===
40
+ def add_missing_columns():
41
+ import sqlite3
42
+ conn = sqlite3.connect("puppets.db")
43
+ cursor = conn.cursor()
44
+ try:
45
+ cursor.execute("ALTER TABLE personalities ADD COLUMN fine_tune_config TEXT DEFAULT '{}';")
46
+ except sqlite3.OperationalError:
47
+ pass # Column already exists
48
+ conn.commit()
49
+ conn.close()
50
 
51
+ add_missing_columns()
 
52
  Base.metadata.create_all(bind=engine)
53
 
54
+ # === GLOBAL STATE ===
 
 
 
55
  _discum_clients: Dict[int, discum.Client] = {}
56
 
57
+ # === UTILS ===
 
58
  @contextmanager
59
  def get_db_session():
 
 
 
 
 
 
 
60
  session = SessionLocal()
61
  try:
62
  yield session
63
+ session.commit()
64
  except Exception:
65
  session.rollback()
66
  raise
67
  finally:
68
  session.close()
69
 
 
 
70
  def validate_discord_id(discord_id: str) -> bool:
71
+ return bool(re.fullmatch(r"^\d{1,20}$", discord_id)) if discord_id else False
 
 
 
 
 
 
 
 
72
 
73
+ def build_final_prompt(personality, channel_instr=""):
74
+ parts = [personality.base_prompt]
75
+ if channel_instr:
76
+ parts.append(f"\n\n[CHANNEL RULES]\n{channel_instr}")
77
+ return "\n".join(parts)
 
78
 
79
+ # === TOOLS ===
80
  class ListPersonalitiesTool(Tool):
 
81
  name = "list_personalities"
82
+ description = "List all personalities"
83
+ inputs = {}
84
+ output_type = "string"
85
+ def forward(self):
 
 
86
  try:
87
+ with get_db_session() as s:
88
+ ps = s.query(Personality).all()
89
+ return json.dumps([{"id": p.id, "name": p.name} for p in ps])
 
 
 
 
90
  except Exception as e:
91
+ return json.dumps({"error": str(e)})
 
 
92
 
93
  class GetChannelPromptTool(Tool):
 
94
  name = "get_channel_prompt"
95
+ description = "Get final prompt for channel and personality"
96
  inputs = {
97
+ "channel_id": {"type": "string"},
98
+ "personality_id": {"type": "integer"}
99
  }
100
  output_type = "string"
101
+ def forward(self, channel_id: str, personality_id: int):
102
+ if not validate_discord_id(channel_id) or personality_id <= 0:
103
+ return json.dumps({"error": "invalid inputs"})
 
 
 
 
 
 
104
  try:
105
+ with get_db_session() as s:
106
+ p = s.query(Personality).filter_by(id=personality_id).first()
107
+ if not p:
108
+ return json.dumps({"error": "personality not found"})
109
+ cfg = s.query(ChannelConfig).filter_by(
110
+ channel_id=channel_id, personality_id=personality_id
 
 
 
 
 
 
111
  ).first()
112
+ instr = cfg.channel_instructions if cfg else ""
113
+ prompt = build_final_prompt(p, instr)
 
 
 
 
 
114
  return json.dumps({
115
+ "prompt": prompt,
116
+ "fine_tune_config": p.fine_tune_config or {}
117
  })
118
  except Exception as e:
119
+ return json.dumps({"error": str(e)})
 
120
 
121
  class InitializePuppetTool(Tool):
 
122
  name = "initialize_puppet"
123
+ description = "Initialize a Discord puppet"
124
  inputs = {
125
+ "puppet_id": {"type": "integer"},
126
+ "token": {"type": "string"}
127
  }
128
  output_type = "string"
129
+ def forward(self, puppet_id: int, token: str):
130
+ if puppet_id <= 0 or len(token) < 50:
131
+ return json.dumps({"error": "invalid puppet_id or token"})
 
 
 
 
 
132
  try:
 
133
  if puppet_id in _discum_clients:
134
+ _discum_clients[puppet_id].close()
135
+ _discum_clients[puppet_id] = discum.Client(token=token, log=False)
 
 
 
 
 
 
 
136
  return json.dumps({"status": "success", "puppet_id": puppet_id})
137
  except Exception as e:
138
+ return json.dumps({"error": str(e)})
 
139
 
140
  class ConfigureChannelTool(Tool):
 
141
  name = "configure_channel"
142
+ description = "Assign personality to channel"
143
  inputs = {
144
+ "puppet_id": {"type": "integer"},
145
+ "channel_id": {"type": "string"},
146
+ "personality_id": {"type": "integer"},
147
+ "instructions": {"type": "string"}
148
  }
149
  output_type = "string"
150
+ def forward(self, puppet_id: int, channel_id: str, personality_id: int, instructions: str):
151
+ if (puppet_id <= 0 or not validate_discord_id(channel_id) or
152
+ personality_id <= 0 or puppet_id not in _discum_clients):
153
+ return json.dumps({"error": "invalid inputs or puppet not initialized"})
 
 
 
 
 
 
 
 
 
 
 
154
  try:
155
+ with get_db_session() as s:
156
+ p = s.query(Personality).filter_by(id=personality_id).first()
157
+ if not p:
158
+ return json.dumps({"error": "personality not found"})
159
+ cfg = ChannelConfig(
 
 
 
 
 
160
  channel_id=channel_id,
161
  personality_id=personality_id,
162
  channel_instructions=instructions.strip() or None
163
  )
164
+ s.add(cfg)
 
165
  return json.dumps({"status": "success"})
166
  except Exception as e:
167
+ return json.dumps({"error": str(e)})
 
168
 
169
  class SendMessageTool(Tool):
 
170
  name = "send_message"
171
+ description = "Send message as puppet"
172
  inputs = {
173
+ "puppet_id": {"type": "integer"},
174
+ "channel_id": {"type": "string"},
175
+ "message": {"type": "string"}
176
  }
177
  output_type = "string"
178
+ def forward(self, puppet_id: int, channel_id: str, message: str):
179
+ if (puppet_id <= 0 or not validate_discord_id(channel_id) or
180
+ not message.strip() or puppet_id not in _discum_clients):
181
+ return json.dumps({"error": "invalid inputs or puppet not initialized"})
 
 
 
 
 
 
 
 
 
 
182
  try:
183
+ _discum_clients[puppet_id].sendMessage(channel_id, message.strip())
 
184
  return json.dumps({"status": "success"})
185
  except Exception as e:
186
+ return json.dumps({"error": str(e)})
 
187
 
 
188
  TOOLS = [
189
  ListPersonalitiesTool(),
190
  GetChannelPromptTool(),
191
  InitializePuppetTool(),
192
  ConfigureChannelTool(),
193
+ SendMessageTool()
194
  ]
195
 
196
+ # === GRADIO UI ===
197
+ def list_personalities_ui():
198
+ tool = ListPersonalitiesTool()
199
+ result = json.loads(tool.forward())
200
+ if "error" in result:
201
+ return f"❌ {result['error']}"
202
+ return "\n".join([f"ID: {p['id']} | {p['name']}" for p in result])
203
+
204
+ def create_personality_ui(name, base_prompt):
205
+ if not name.strip() or not base_prompt.strip():
206
+ return "❌ Name and base prompt required", ""
207
  try:
208
+ with get_db_session() as s:
209
+ p = Personality(name=name.strip(), base_prompt=base_prompt.strip())
210
+ s.add(p)
211
+ s.flush()
212
+ return f"βœ… Created! ID: {p.id}", str(p.id)
213
  except Exception as e:
214
+ return f"❌ Failed: {str(e)}", ""
215
 
216
+ def edit_personality_ui(pid, name, base_prompt, ft_json):
217
+ if not pid:
218
+ return "❌ Personality ID required"
219
+ try:
220
+ pid = int(pid)
221
+ ft = json.loads(ft_json) if ft_json.strip() else {}
222
+ with get_db_session() as s:
223
+ p = s.query(Personality).filter_by(id=pid).first()
224
+ if not p:
225
+ return "❌ Not found"
226
+ if name.strip(): p.name = name.strip()
227
+ if base_prompt.strip(): p.base_prompt = base_prompt.strip()
228
+ p.fine_tune_config = ft
229
+ return "βœ… Updated"
230
+ except Exception as e:
231
+ return f"❌ Error: {str(e)}"
232
 
233
+ def initialize_puppet_ui(puppet_id, token):
234
+ try:
235
+ pid = int(puppet_id)
236
+ tool = InitializePuppetTool()
237
+ result = json.loads(tool.forward(pid, token))
238
+ if "error" in result:
239
+ return f"❌ {result['error']}"
240
+ return f"βœ… Puppet {pid} initialized"
241
+ except Exception as e:
242
+ return f"❌ {str(e)}"
 
 
 
 
243
 
244
+ def configure_channel_ui(puppet_id, channel_id, personality_id, instructions):
245
+ try:
246
+ pid = int(puppet_id)
247
+ per_id = int(personality_id)
248
+ tool = ConfigureChannelTool()
249
+ result = json.loads(tool.forward(pid, channel_id, per_id, instructions))
250
+ if "error" in result:
251
+ return f"❌ {result['error']}"
252
+ return "βœ… Channel configured"
253
+ except Exception as e:
254
+ return f"❌ {str(e)}"
255
 
256
+ def send_message_ui(puppet_id, channel_id, message):
257
+ try:
258
+ pid = int(puppet_id)
259
+ tool = SendMessageTool()
260
+ result = json.loads(tool.forward(pid, channel_id, message))
261
+ if "error" in result:
262
+ return f"❌ {result['error']}"
263
+ return "βœ… Message sent"
264
+ except Exception as e:
265
+ return f"❌ {str(e)}"
266
 
267
+ with gr.Blocks(title="Puppet Master") as demo:
268
+ gr.Markdown("# 🎭 Puppet Master β€” Discord Puppet Orchestrator")
269
+
270
+ with gr.Tabs():
271
+ with gr.Tab("🧠 Personalities"):
272
+ with gr.Row():
273
+ with gr.Column():
274
+ gr.Markdown("### Create")
275
+ cp_name = gr.Textbox(label="Name")
276
+ cp_prompt = gr.Textbox(label="Base Prompt", lines=3)
277
+ cp_btn = gr.Button("Create")
278
+ cp_status = gr.Textbox(label="Status", interactive=False)
279
+ cp_id = gr.Textbox(label="ID", interactive=False)
280
+ with gr.Column():
281
+ gr.Markdown("### Edit")
282
+ ep_id = gr.Textbox(label="Personality ID")
283
+ ep_name = gr.Textbox(label="New Name (optional)")
284
+ ep_prompt = gr.Textbox(label="New Prompt (optional)", lines=3)
285
+ ep_ft = gr.Textbox(label="Fine-tune Config (JSON)", lines=2)
286
+ ep_btn = gr.Button("Update")
287
+ ep_status = gr.Textbox(label="Status", interactive=False)
288
+ gr.Markdown("### All Personalities")
289
+ list_btn = gr.Button("Refresh")
290
+ list_out = gr.Textbox(label="List", lines=6, interactive=False)
291
+ cp_btn.click(create_personality_ui, [cp_name, cp_prompt], [cp_status, cp_id])
292
+ ep_btn.click(edit_personality_ui, [ep_id, ep_name, ep_prompt, ep_ft], ep_status)
293
+ list_btn.click(list_personalities_ui, outputs=list_out)
294
+
295
+ with gr.Tab("πŸ€– Puppets"):
296
+ with gr.Row():
297
+ with gr.Column():
298
+ gr.Markdown("### Initialize Puppet")
299
+ ip_id = gr.Textbox(label="Puppet ID (e.g., 1)")
300
+ ip_token = gr.Textbox(label="Discord Token", type="password")
301
+ ip_btn = gr.Button("Initialize")
302
+ ip_status = gr.Textbox(label="Status", interactive=False)
303
+ with gr.Column():
304
+ gr.Markdown("### Configure Channel")
305
+ cc_puppet = gr.Textbox(label="Puppet ID")
306
+ cc_ch = gr.Textbox(label="Channel ID")
307
+ cc_pid = gr.Textbox(label="Personality ID")
308
+ cc_instr = gr.Textbox(label="Channel Instructions", lines=2)
309
+ cc_btn = gr.Button("Assign")
310
+ cc_status = gr.Textbox(label="Status", interactive=False)
311
+ ip_btn.click(initialize_puppet_ui, [ip_id, ip_token], ip_status)
312
+ cc_btn.click(configure_channel_ui, [cc_puppet, cc_ch, cc_pid, cc_instr], cc_status)
313
+
314
+ with gr.Tab("πŸ“€ Send Message"):
315
+ with gr.Row():
316
+ with gr.Column():
317
+ sm_puppet = gr.Textbox(label="Puppet ID")
318
+ sm_ch = gr.Textbox(label="Channel ID")
319
+ sm_msg = gr.Textbox(label="Message", lines=3)
320
+ sm_btn = gr.Button("Send")
321
+ sm_status = gr.Textbox(label="Status", interactive=False)
322
+ sm_btn.click(send_message_ui, [sm_puppet, sm_ch, sm_msg], sm_status)
323
 
 
324
  if __name__ == "__main__":
325
+ demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)