Przemyslaw Chachaj commited on
Commit
00f98d2
·
1 Parent(s): 7271b67

Full Behavior Changer MCP agent

Browse files
Files changed (2) hide show
  1. app.py +317 -17
  2. requirements.txt +2 -0
app.py CHANGED
@@ -1,37 +1,337 @@
 
 
 
 
 
 
1
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
 
4
- def create_contract(link: str, category: str = "general") -> dict:
 
 
 
 
 
 
 
 
 
 
 
 
5
  """
6
- Create a simple habit contract from a reel link.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
- Args:
9
- link: URL to the reel / content.
10
- category: Area of life (e.g. career, health, relationships).
11
 
12
- Returns:
13
- JSON with a fake uid, echo of params and first tiny step.
 
 
 
 
14
  """
15
- if not link:
16
- return {"error": "link is required"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  return {
19
- "uid": f"demo_{category}",
20
  "link": link,
21
  "category": category,
22
- "next_step": "Watch the reel and take ONE practical action today."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  }
24
 
25
 
 
 
 
 
26
  with gr.Blocks() as demo:
27
- gr.Markdown("# Behavior Changer – MCP Demo")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
- link = gr.Textbox(label="Reel/link", placeholder="https://instagram.com/reel/...")
30
- category = gr.Textbox(label="Category", value="career")
31
- out = gr.JSON(label="Result JSON")
 
 
32
 
33
- btn = gr.Button("Generate Contract")
34
- btn.click(create_contract, [link, category], out)
 
 
 
35
 
 
 
 
 
 
 
 
36
 
37
  demo.launch(mcp_server=True)
 
1
+ import os
2
+ import json
3
+ import uuid
4
+ import datetime
5
+ from typing import Optional, Dict, Any
6
+
7
  import gradio as gr
8
+ import yt_dlp
9
+ from openai import OpenAI
10
+
11
+ # =====================================
12
+ # CONFIG
13
+ # =====================================
14
+
15
+ DATA_DIR = "/data/contracts"
16
+ os.makedirs(DATA_DIR, exist_ok=True)
17
+
18
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
19
+ client = OpenAI(api_key=OPENAI_API_KEY)
20
+
21
+
22
+ # =====================================
23
+ # STORAGE UTILS
24
+ # =====================================
25
+
26
+ def _contract_path(uid: str) -> str:
27
+ return os.path.join(DATA_DIR, f"{uid}.json")
28
+
29
+
30
+ def save_contract(contract: Dict[str, Any]) -> None:
31
+ uid = contract["uid"]
32
+ path = _contract_path(uid)
33
+ with open(path, "w", encoding="utf-8") as f:
34
+ json.dump(contract, f, ensure_ascii=False, indent=2)
35
 
36
 
37
+ def load_contract(uid: str) -> Dict[str, Any]:
38
+ path = _contract_path(uid)
39
+ if not os.path.exists(path):
40
+ raise ValueError(f"Contract with uid={uid} not found.")
41
+ with open(path, "r", encoding="utf-8") as f:
42
+ return json.load(f)
43
+
44
+
45
+ # =====================================
46
+ # CORE TOOLS
47
+ # =====================================
48
+
49
+ def transcribe_reel(link: str) -> str:
50
  """
51
+ Download a reel/video from a link (e.g. Instagram) and transcribe audio using OpenAI Whisper.
52
+ Returns plain text transcript.
53
+ """
54
+ if not OPENAI_API_KEY:
55
+ raise ValueError("OPENAI_API_KEY not set in environment.")
56
+
57
+ # Download audio/video with yt-dlp
58
+ tmp_id = str(uuid.uuid4())
59
+ out_tmpl = f"/tmp/{tmp_id}.%(ext)s"
60
+
61
+ ydl_opts = {
62
+ "format": "mp4/bestaudio/best",
63
+ "outtmpl": out_tmpl,
64
+ "quiet": True,
65
+ }
66
+
67
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
68
+ info = ydl.extract_info(link, download=True)
69
+ filename = ydl.prepare_filename(info)
70
+
71
+ # Transcription via OpenAI
72
+ with open(filename, "rb") as f:
73
+ transcript = client.audio.transcriptions.create(
74
+ model="gpt-4o-transcribe",
75
+ file=f
76
+ )
77
 
78
+ text = transcript.text if hasattr(transcript, "text") else str(transcript)
79
+ return text
 
80
 
81
+
82
+ def extract_habits(transcript: str, category: str = "general") -> Dict[str, Any]:
83
+ """
84
+ Analyze transcript and produce:
85
+ - goal (one sentence)
86
+ - 7 simple actionable steps (list of strings)
87
  """
88
+ if not OPENAI_API_KEY:
89
+ raise ValueError("OPENAI_API_KEY not set in environment.")
90
+
91
+ if not transcript or not transcript.strip():
92
+ # Fallback generic plan
93
+ return {
94
+ "goal": f"Improve your {category} through one small action per day.",
95
+ "steps": [
96
+ "Pick one tiny action related to your goal and do it today.",
97
+ "Repeat the same action and make it slightly harder.",
98
+ "Share your progress with someone (message or note).",
99
+ "Add 10–15 minutes of focused deep work on this goal.",
100
+ "Remove one distraction that blocks this goal.",
101
+ "Create a simple checklist for next week.",
102
+ "Summarize what you learned in 3–5 sentences."
103
+ ]
104
+ }
105
+
106
+ system_msg = (
107
+ "You are a behavior change coach. "
108
+ "User gives you a transcript of a short motivational / educational video. "
109
+ "Your job is to extract ONE clear goal and EXACTLY 7 simple daily actions "
110
+ "that move the user toward that goal. "
111
+ "Return STRICT JSON with keys: goal (string), steps (array of 7 short strings). "
112
+ "Do NOT add any explanation, only JSON."
113
+ )
114
+
115
+ user_msg = (
116
+ f"Category: {category}\n\n"
117
+ f"Transcript (may be truncated):\n"
118
+ f"{transcript[:4000]}"
119
+ )
120
+
121
+ resp = client.chat.completions.create(
122
+ model="gpt-4.1-mini",
123
+ messages=[
124
+ {"role": "system", "content": system_msg},
125
+ {"role": "user", "content": user_msg},
126
+ ],
127
+ temperature=0.2,
128
+ )
129
+
130
+ raw = resp.choices[0].message.content
131
+
132
+ # Try to parse JSON even if model wrapped it in text/code fences
133
+ raw = raw.strip()
134
+ start = raw.find("{")
135
+ end = raw.rfind("}")
136
+ if start == -1 or end == -1:
137
+ raise ValueError(f"Cannot parse JSON from model output: {raw}")
138
+ json_str = raw[start:end + 1]
139
+
140
+ data = json.loads(json_str)
141
+
142
+ # Minimal validation / normalization
143
+ goal = data.get("goal") or f"Improve your {category}."
144
+ steps = data.get("steps") or []
145
+ steps = [s for s in steps if isinstance(s, str)]
146
+ if len(steps) < 7:
147
+ # pad if needed
148
+ while len(steps) < 7:
149
+ steps.append("Repeat a small action that moves you forward.")
150
+ else:
151
+ steps = steps[:7]
152
+
153
+ return {"goal": goal, "steps": steps}
154
+
155
+
156
+ def create_contract(
157
+ link: str,
158
+ category: str = "general",
159
+ uid: Optional[str] = None,
160
+ auto_transcribe: bool = True
161
+ ) -> Dict[str, Any]:
162
+ """
163
+ Create a habit contract from a video link.
164
+
165
+ - Downloads + transcribes the video (if auto_transcribe=True)
166
+ - Extracts goal + 7 habit steps
167
+ - Saves contract to storage
168
+ - Returns public contract summary
169
+ """
170
+ if uid is None or not uid.strip():
171
+ uid = f"{category}_{uuid.uuid4().hex[:8]}"
172
+
173
+ transcript = ""
174
+ if auto_transcribe:
175
+ try:
176
+ transcript = transcribe_reel(link)
177
+ except Exception as e:
178
+ # Fallback: no transcript, but do not fail hard
179
+ transcript = ""
180
+ print(f"[WARN] Transcription failed: {e}")
181
+
182
+ habits = extract_habits(transcript, category=category)
183
+
184
+ contract = {
185
+ "uid": uid,
186
+ "link": link,
187
+ "category": category,
188
+ "goal": habits["goal"],
189
+ "steps": habits["steps"],
190
+ "current_index": 0,
191
+ "history": [],
192
+ "created_at": datetime.datetime.utcnow().isoformat() + "Z",
193
+ }
194
+
195
+ save_contract(contract)
196
 
197
  return {
198
+ "uid": uid,
199
  "link": link,
200
  "category": category,
201
+ "goal": contract["goal"],
202
+ "first_step": contract["steps"][0],
203
+ "total_steps": len(contract["steps"]),
204
+ }
205
+
206
+
207
+ def next_step(uid: str) -> Dict[str, Any]:
208
+ """
209
+ Return the next actionable step for given contract.
210
+ """
211
+ contract = load_contract(uid)
212
+ idx = contract.get("current_index", 0)
213
+ steps = contract.get("steps", [])
214
+
215
+ if idx >= len(steps):
216
+ return {
217
+ "uid": uid,
218
+ "done": True,
219
+ "message": "All steps completed.",
220
+ "goal": contract.get("goal", ""),
221
+ }
222
+
223
+ return {
224
+ "uid": uid,
225
+ "done": False,
226
+ "goal": contract.get("goal", ""),
227
+ "step_index": idx,
228
+ "step": steps[idx],
229
+ "remaining_steps": len(steps) - idx,
230
+ }
231
+
232
+
233
+ def mark_done(uid: str) -> Dict[str, Any]:
234
+ """
235
+ Mark current step as done and advance to next one.
236
+ """
237
+ contract = load_contract(uid)
238
+ idx = contract.get("current_index", 0)
239
+ steps = contract.get("steps", [])
240
+
241
+ now = datetime.datetime.utcnow().isoformat() + "Z"
242
+
243
+ if idx < len(steps):
244
+ current_step = steps[idx]
245
+ contract.setdefault("history", []).append(
246
+ {"index": idx, "step": current_step, "done_at": now}
247
+ )
248
+ contract["current_index"] = idx + 1
249
+ save_contract(contract)
250
+
251
+ return next_step(uid)
252
+
253
+
254
+ def report(uid: str) -> Dict[str, Any]:
255
+ """
256
+ Return a summary of contract progress.
257
+ """
258
+ contract = load_contract(uid)
259
+ steps = contract.get("steps", [])
260
+ idx = contract.get("current_index", 0)
261
+ history = contract.get("history", [])
262
+
263
+ total = len(steps)
264
+ done = min(idx, total)
265
+ remaining = max(total - done, 0)
266
+ progress = (done / total * 100.0) if total > 0 else 0.0
267
+
268
+ return {
269
+ "uid": uid,
270
+ "goal": contract.get("goal", ""),
271
+ "category": contract.get("category", ""),
272
+ "total_steps": total,
273
+ "done_steps": done,
274
+ "remaining_steps": remaining,
275
+ "progress_percent": round(progress, 1),
276
+ "history": history,
277
+ "current_step": steps[idx] if idx < total else None,
278
  }
279
 
280
 
281
+ # =====================================
282
+ # GRADIO UI + MCP EXPOSURE
283
+ # =====================================
284
+
285
  with gr.Blocks() as demo:
286
+ gr.Markdown(
287
+ "# Behavior Changer – Habit Agent (MCP)\n"
288
+ "Tworzy kontrakty nawyków z linków do Reels / video, "
289
+ "rozbija na kroki i udostępnia jako MCP tools."
290
+ )
291
+
292
+ with gr.Tab("Create Contract"):
293
+ link_in = gr.Textbox(label="Reel / Video URL")
294
+ category_in = gr.Textbox(label="Category", value="career")
295
+ uid_in = gr.Textbox(label="Custom UID (optional)", value="")
296
+ output_create = gr.JSON(label="Contract summary")
297
+
298
+ btn_create = gr.Button("Create Contract")
299
+ btn_create.click(
300
+ create_contract,
301
+ inputs=[link_in, category_in, uid_in],
302
+ outputs=output_create,
303
+ )
304
+
305
+ with gr.Tab("Next Step"):
306
+ uid_next_in = gr.Textbox(label="Contract UID")
307
+ output_next = gr.JSON(label="Next step")
308
+ btn_next = gr.Button("Get Next Step")
309
+ btn_next.click(next_step, inputs=[uid_next_in], outputs=output_next)
310
+
311
+ with gr.Tab("Mark Done"):
312
+ uid_done_in = gr.Textbox(label="Contract UID")
313
+ output_done = gr.JSON(label="After marking done (next step)")
314
+ btn_done = gr.Button("Mark Current Step as Done")
315
+ btn_done.click(mark_done, inputs=[uid_done_in], outputs=output_done)
316
 
317
+ with gr.Tab("Report"):
318
+ uid_report_in = gr.Textbox(label="Contract UID")
319
+ output_report = gr.JSON(label="Progress report")
320
+ btn_report = gr.Button("Get Report")
321
+ btn_report.click(report, inputs=[uid_report_in], outputs=output_report)
322
 
323
+ with gr.Tab("Debug: Transcribe Only"):
324
+ link_tr_in = gr.Textbox(label="Reel / Video URL")
325
+ output_tr = gr.Textbox(label="Transcript (preview)")
326
+ btn_tr = gr.Button("Transcribe")
327
+ btn_tr.click(transcribe_reel, inputs=[link_tr_in], outputs=output_tr)
328
 
329
+ # MCP tools – exposed via /gradio_api/mcp
330
+ gr.api(create_contract)
331
+ gr.api(next_step)
332
+ gr.api(mark_done)
333
+ gr.api(report)
334
+ gr.api(transcribe_reel)
335
+ gr.api(extract_habits)
336
 
337
  demo.launch(mcp_server=True)
requirements.txt CHANGED
@@ -1 +1,3 @@
1
  gradio[mcp]
 
 
 
1
  gradio[mcp]
2
+ yt-dlp
3
+ openai>=1.40.0