gitcast 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ai/__init__.py +0 -0
- ai/formatter.py +59 -0
- ai/generator.py +604 -0
- ai/prompts.py +197 -0
- ai/viral_patterns.py +75 -0
- api/__init__.py +0 -0
- api/analytics.py +48 -0
- api/auth.py +49 -0
- api/auth_middleware.py +129 -0
- api/auth_routes.py +117 -0
- api/monitoring.py +56 -0
- api/payload.py +253 -0
- api/ratelimit.py +9 -0
- api/routes.py +1565 -0
- api/server.py +162 -0
- api/validators.py +101 -0
- assets/__init__.py +1 -0
- assets/favicon-16x16.png +0 -0
- assets/favicon-32x32.png +0 -0
- assets/favicon-64x64.png +0 -0
- assets/favicon.ico +0 -0
- assets/icon.png +0 -0
- cli/.env.example +26 -0
- cli/__init__.py +1 -0
- cli/gitcast.py +79 -0
- config/__init__.py +0 -0
- config/settings.py +213 -0
- core/__init__.py +0 -0
- core/capture.py +258 -0
- core/codebase_reader.py +90 -0
- core/framing.py +86 -0
- core/hotkey.py +21 -0
- core/log_stream.py +50 -0
- core/ocr.py +173 -0
- core/screenshot_session.py +274 -0
- core/security.py +126 -0
- core/tray.py +54 -0
- gitcast-1.0.0.dist-info/LICENSE +21 -0
- gitcast-1.0.0.dist-info/METADATA +67 -0
- gitcast-1.0.0.dist-info/RECORD +61 -0
- gitcast-1.0.0.dist-info/WHEEL +5 -0
- gitcast-1.0.0.dist-info/entry_points.txt +2 -0
- gitcast-1.0.0.dist-info/top_level.txt +10 -0
- publisher/__init__.py +0 -0
- publisher/clipboard.py +44 -0
- publisher/twitter.py +100 -0
- storage/__init__.py +0 -0
- storage/cleanup.py +60 -0
- storage/engagement.py +114 -0
- storage/insights.py +203 -0
- storage/key_manager.py +45 -0
- storage/logger.py +208 -0
- storage/metrics.py +119 -0
- storage/sprint.py +40 -0
- storage/streak.py +0 -0
- storage/supabase_client.py +25 -0
- storage/tone_memory.py +139 -0
- ui/__init__.py +0 -0
- web/__init__.py +1 -0
- web/index.html +4994 -0
- web/landing.html +925 -0
storage/logger.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from storage.supabase_client import get_client
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _is_supabase_user(user_id: Optional[str]) -> bool:
|
|
9
|
+
try:
|
|
10
|
+
UUID(str(user_id))
|
|
11
|
+
return True
|
|
12
|
+
except (TypeError, ValueError):
|
|
13
|
+
return False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _normalize_entry(entry: dict) -> dict:
|
|
17
|
+
timestamp = entry.get("timestamp") or entry.get("created_at") or ""
|
|
18
|
+
tweet_url = entry.get("tweet_url", "") or ""
|
|
19
|
+
return {
|
|
20
|
+
**entry,
|
|
21
|
+
"id": str(entry.get("id", "")),
|
|
22
|
+
"timestamp": timestamp,
|
|
23
|
+
"posted_verified": bool(entry.get("posted_verified", False)),
|
|
24
|
+
"posted_declined": bool(entry.get("declined", False)),
|
|
25
|
+
"post_url": tweet_url,
|
|
26
|
+
"verified_at": entry.get("verified_at", "") or "",
|
|
27
|
+
"metrics": {
|
|
28
|
+
"impressions": int(entry.get("impressions") or 0),
|
|
29
|
+
"likes": int(entry.get("likes") or 0),
|
|
30
|
+
"comments": int(entry.get("comments") or 0),
|
|
31
|
+
"reposts": int(entry.get("reposts") or 0),
|
|
32
|
+
"hashtags": entry.get("hashtags") or [],
|
|
33
|
+
"platform": entry.get("platform") or "",
|
|
34
|
+
"days_after_post": int(entry.get("days_after_post") or 0),
|
|
35
|
+
"measured_at": entry.get("metrics_saved_at") or "",
|
|
36
|
+
} if entry.get("metrics_saved") else {},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def save_posts(posts: list, user_id: Optional[str] = None) -> None:
|
|
41
|
+
print("[Logger] save_posts is deprecated for Supabase storage")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def log_post(
|
|
45
|
+
post_text: str,
|
|
46
|
+
format_key: str,
|
|
47
|
+
screenshot_path: str,
|
|
48
|
+
tweet_url: str = "",
|
|
49
|
+
tweet_id: str = "",
|
|
50
|
+
fallback: bool = False,
|
|
51
|
+
timestamp: str = "",
|
|
52
|
+
user_id: Optional[str] = None,
|
|
53
|
+
provider_used: str = "",
|
|
54
|
+
) -> Optional[str]:
|
|
55
|
+
if not user_id:
|
|
56
|
+
raise ValueError("user_id is required")
|
|
57
|
+
if not _is_supabase_user(user_id):
|
|
58
|
+
print("[Logger] Local user detected; skipping Supabase post log")
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
payload = {
|
|
62
|
+
"user_id": user_id,
|
|
63
|
+
"post_text": post_text,
|
|
64
|
+
"format_key": format_key,
|
|
65
|
+
"provider_used": provider_used or ("fallback" if fallback else ""),
|
|
66
|
+
"platform": "twitter",
|
|
67
|
+
"tweet_url": tweet_url,
|
|
68
|
+
"tweet_id": tweet_id,
|
|
69
|
+
}
|
|
70
|
+
if timestamp:
|
|
71
|
+
payload["timestamp"] = timestamp
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
response = get_client().table("posts").insert(payload).execute()
|
|
75
|
+
row = response.data[0] if response.data else {}
|
|
76
|
+
print("[Logger] Post metadata logged to Supabase")
|
|
77
|
+
return row.get("id")
|
|
78
|
+
except Exception as e:
|
|
79
|
+
print(f"[Logger] Failed to log post metadata: {e}")
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def load_posts(user_id: str) -> list:
|
|
84
|
+
if not _is_supabase_user(user_id):
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
response = (
|
|
89
|
+
get_client()
|
|
90
|
+
.table("posts")
|
|
91
|
+
.select("*")
|
|
92
|
+
.eq("user_id", user_id)
|
|
93
|
+
.order("timestamp", desc=True)
|
|
94
|
+
.execute()
|
|
95
|
+
)
|
|
96
|
+
return [_normalize_entry(entry) for entry in (response.data or [])]
|
|
97
|
+
except Exception as e:
|
|
98
|
+
print(f"[Logger] Failed to load posts: {e}")
|
|
99
|
+
return []
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def verify_post(post_id: str, post_url: str = "", user_id: Optional[str] = None) -> dict:
|
|
103
|
+
if not user_id:
|
|
104
|
+
return {"success": False, "error": "user_id is required"}
|
|
105
|
+
if not _is_supabase_user(user_id):
|
|
106
|
+
return {"success": False, "error": "local history is not backed by Supabase"}
|
|
107
|
+
|
|
108
|
+
payload = {
|
|
109
|
+
"posted_verified": True,
|
|
110
|
+
"declined": False,
|
|
111
|
+
"verified_at": datetime.now().isoformat(),
|
|
112
|
+
}
|
|
113
|
+
if post_url:
|
|
114
|
+
payload["tweet_url"] = post_url
|
|
115
|
+
|
|
116
|
+
response = (
|
|
117
|
+
get_client()
|
|
118
|
+
.table("posts")
|
|
119
|
+
.update(payload)
|
|
120
|
+
.eq("id", post_id)
|
|
121
|
+
.eq("user_id", user_id)
|
|
122
|
+
.execute()
|
|
123
|
+
)
|
|
124
|
+
if response.data:
|
|
125
|
+
return {"success": True, "post_id": post_id}
|
|
126
|
+
return {"success": False, "error": "post not found"}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def decline_post(post_id: str, user_id: Optional[str] = None) -> dict:
|
|
130
|
+
if not user_id:
|
|
131
|
+
return {"success": False, "error": "user_id is required"}
|
|
132
|
+
if not _is_supabase_user(user_id):
|
|
133
|
+
return {"success": False, "error": "local history is not backed by Supabase"}
|
|
134
|
+
|
|
135
|
+
response = (
|
|
136
|
+
get_client()
|
|
137
|
+
.table("posts")
|
|
138
|
+
.update({"declined": True})
|
|
139
|
+
.eq("id", post_id)
|
|
140
|
+
.eq("user_id", user_id)
|
|
141
|
+
.execute()
|
|
142
|
+
)
|
|
143
|
+
if response.data:
|
|
144
|
+
return {"success": True, "post_id": post_id}
|
|
145
|
+
return {"success": False, "error": "post not found"}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_unverified_posts(user_id: str) -> list:
|
|
149
|
+
posts = [
|
|
150
|
+
entry for entry in load_posts(user_id)
|
|
151
|
+
if not entry.get("posted_verified") and not entry.get("posted_declined")
|
|
152
|
+
]
|
|
153
|
+
return sorted(posts, key=lambda item: item.get("timestamp", ""), reverse=True)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def get_streak(user_id: str) -> dict:
|
|
157
|
+
posts = [post for post in load_posts(user_id) if not post.get("posted_declined")]
|
|
158
|
+
if not posts:
|
|
159
|
+
return {"current_streak": 0, "best_streak": 0, "total_posts": 0, "last_post_date": ""}
|
|
160
|
+
|
|
161
|
+
post_dates = set()
|
|
162
|
+
for entry in posts:
|
|
163
|
+
try:
|
|
164
|
+
post_dates.add(datetime.fromisoformat(str(entry["timestamp"]).replace("Z", "+00:00")).date())
|
|
165
|
+
except (KeyError, ValueError):
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
if not post_dates:
|
|
169
|
+
return {"current_streak": 0, "best_streak": 0, "total_posts": len(posts), "last_post_date": ""}
|
|
170
|
+
|
|
171
|
+
sorted_dates = sorted(post_dates, reverse=True)
|
|
172
|
+
last_post_date = sorted_dates[0]
|
|
173
|
+
today = datetime.now().date()
|
|
174
|
+
if last_post_date < today - timedelta(days=1):
|
|
175
|
+
return {
|
|
176
|
+
"current_streak": 0,
|
|
177
|
+
"best_streak": 0,
|
|
178
|
+
"total_posts": len(posts),
|
|
179
|
+
"last_post_date": str(last_post_date),
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
streak = 1
|
|
183
|
+
for i in range(1, len(sorted_dates)):
|
|
184
|
+
if sorted_dates[i] == sorted_dates[i - 1] - timedelta(days=1):
|
|
185
|
+
streak += 1
|
|
186
|
+
else:
|
|
187
|
+
break
|
|
188
|
+
|
|
189
|
+
best_streak = 1
|
|
190
|
+
running = 1
|
|
191
|
+
for i in range(1, len(sorted_dates)):
|
|
192
|
+
if sorted_dates[i] == sorted_dates[i - 1] - timedelta(days=1):
|
|
193
|
+
running += 1
|
|
194
|
+
else:
|
|
195
|
+
best_streak = max(best_streak, running)
|
|
196
|
+
running = 1
|
|
197
|
+
best_streak = max(best_streak, running)
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
"current_streak": streak,
|
|
201
|
+
"best_streak": best_streak,
|
|
202
|
+
"total_posts": len(posts),
|
|
203
|
+
"last_post_date": str(last_post_date),
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
if __name__ == "__main__":
|
|
208
|
+
print("[Logger] Supabase logger module loaded")
|
storage/metrics.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from storage.supabase_client import get_client
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _is_supabase_user(user_id: Optional[str]) -> bool:
|
|
9
|
+
try:
|
|
10
|
+
UUID(str(user_id))
|
|
11
|
+
return True
|
|
12
|
+
except (TypeError, ValueError):
|
|
13
|
+
return False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _normalise_metrics(post_id: str, metrics: dict) -> dict:
|
|
17
|
+
return {
|
|
18
|
+
"post_id": post_id,
|
|
19
|
+
"impressions": int(metrics.get("impressions", 0)),
|
|
20
|
+
"likes": int(metrics.get("likes", 0)),
|
|
21
|
+
"comments": int(metrics.get("comments", 0)),
|
|
22
|
+
"reposts": int(metrics.get("reposts", 0)),
|
|
23
|
+
"hashtags": [str(tag).strip()[:40] for tag in metrics.get("hashtags", []) if str(tag).strip()][:10],
|
|
24
|
+
"platform": str(metrics.get("platform", "")).strip()[:40],
|
|
25
|
+
"measured_at": metrics.get("measured_at") or datetime.now().isoformat(),
|
|
26
|
+
"days_after_post": int(metrics.get("days_after_post", 1)),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def save_metrics(post_id: str, metrics: dict, user_id: Optional[str] = None) -> dict:
|
|
31
|
+
if not user_id:
|
|
32
|
+
return {"success": False, "error": "user_id is required"}
|
|
33
|
+
if not _is_supabase_user(user_id):
|
|
34
|
+
return {"success": False, "error": "local metrics are not backed by Supabase"}
|
|
35
|
+
|
|
36
|
+
entry = _normalise_metrics(post_id, metrics)
|
|
37
|
+
payload = {
|
|
38
|
+
"impressions": entry["impressions"],
|
|
39
|
+
"likes": entry["likes"],
|
|
40
|
+
"comments": entry["comments"],
|
|
41
|
+
"reposts": entry["reposts"],
|
|
42
|
+
"hashtags": entry["hashtags"],
|
|
43
|
+
"platform": entry["platform"] or "twitter",
|
|
44
|
+
"days_after_post": entry["days_after_post"],
|
|
45
|
+
"metrics_saved": True,
|
|
46
|
+
"metrics_saved_at": entry["measured_at"],
|
|
47
|
+
}
|
|
48
|
+
response = (
|
|
49
|
+
get_client()
|
|
50
|
+
.table("posts")
|
|
51
|
+
.update(payload)
|
|
52
|
+
.eq("id", post_id)
|
|
53
|
+
.eq("user_id", user_id)
|
|
54
|
+
.execute()
|
|
55
|
+
)
|
|
56
|
+
if not response.data:
|
|
57
|
+
return {"success": False, "error": "post not found"}
|
|
58
|
+
return {"success": True}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_all_metrics(user_id: Optional[str] = None) -> list:
|
|
62
|
+
if not user_id or not _is_supabase_user(user_id):
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
response = (
|
|
66
|
+
get_client()
|
|
67
|
+
.table("posts")
|
|
68
|
+
.select("id,impressions,likes,comments,reposts,hashtags,platform,metrics_saved_at,days_after_post")
|
|
69
|
+
.eq("user_id", user_id)
|
|
70
|
+
.eq("metrics_saved", True)
|
|
71
|
+
.execute()
|
|
72
|
+
)
|
|
73
|
+
entries = []
|
|
74
|
+
for row in response.data or []:
|
|
75
|
+
entries.append({
|
|
76
|
+
"post_id": row["id"],
|
|
77
|
+
"impressions": row.get("impressions") or 0,
|
|
78
|
+
"likes": row.get("likes") or 0,
|
|
79
|
+
"comments": row.get("comments") or 0,
|
|
80
|
+
"reposts": row.get("reposts") or 0,
|
|
81
|
+
"hashtags": row.get("hashtags") or [],
|
|
82
|
+
"platform": row.get("platform") or "",
|
|
83
|
+
"measured_at": row.get("metrics_saved_at") or "",
|
|
84
|
+
"days_after_post": row.get("days_after_post") or 0,
|
|
85
|
+
})
|
|
86
|
+
return entries
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_metrics(post_id: str, user_id: Optional[str] = None) -> dict:
|
|
90
|
+
if not user_id or not _is_supabase_user(user_id):
|
|
91
|
+
return {}
|
|
92
|
+
|
|
93
|
+
response = (
|
|
94
|
+
get_client()
|
|
95
|
+
.table("posts")
|
|
96
|
+
.select("id,impressions,likes,comments,reposts,hashtags,platform,metrics_saved_at,days_after_post")
|
|
97
|
+
.eq("id", post_id)
|
|
98
|
+
.eq("user_id", user_id)
|
|
99
|
+
.eq("metrics_saved", True)
|
|
100
|
+
.execute()
|
|
101
|
+
)
|
|
102
|
+
if not response.data:
|
|
103
|
+
return {}
|
|
104
|
+
row = response.data[0]
|
|
105
|
+
return {
|
|
106
|
+
"post_id": row["id"],
|
|
107
|
+
"impressions": row.get("impressions") or 0,
|
|
108
|
+
"likes": row.get("likes") or 0,
|
|
109
|
+
"comments": row.get("comments") or 0,
|
|
110
|
+
"reposts": row.get("reposts") or 0,
|
|
111
|
+
"hashtags": row.get("hashtags") or [],
|
|
112
|
+
"platform": row.get("platform") or "",
|
|
113
|
+
"measured_at": row.get("metrics_saved_at") or "",
|
|
114
|
+
"days_after_post": row.get("days_after_post") or 0,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
print("[Metrics] Supabase metrics module loaded")
|
storage/sprint.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from config.settings import SPRINT_LOG
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def log_sprint_capture(
|
|
6
|
+
git_diff: str,
|
|
7
|
+
ocr_text: str,
|
|
8
|
+
raw_thought: str,
|
|
9
|
+
timestamp: str,
|
|
10
|
+
) -> None:
|
|
11
|
+
entry = {
|
|
12
|
+
"timestamp": timestamp,
|
|
13
|
+
"raw_thought": raw_thought,
|
|
14
|
+
"git_diff": git_diff[:1000],
|
|
15
|
+
"ocr_text": ocr_text[:500],
|
|
16
|
+
}
|
|
17
|
+
with open(SPRINT_LOG, "a", encoding="utf-8") as f:
|
|
18
|
+
f.write(json.dumps(entry) + "\n")
|
|
19
|
+
print(f"[Sprint] Capture logged to {SPRINT_LOG}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_sprint_log() -> list[dict]:
|
|
23
|
+
if not SPRINT_LOG.exists():
|
|
24
|
+
return []
|
|
25
|
+
entries = []
|
|
26
|
+
with open(SPRINT_LOG, "r", encoding="utf-8") as f:
|
|
27
|
+
for line in f:
|
|
28
|
+
line = line.strip()
|
|
29
|
+
if line:
|
|
30
|
+
try:
|
|
31
|
+
entries.append(json.loads(line))
|
|
32
|
+
except json.JSONDecodeError:
|
|
33
|
+
continue
|
|
34
|
+
return entries
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def clear_sprint_log() -> None:
|
|
38
|
+
if SPRINT_LOG.exists():
|
|
39
|
+
SPRINT_LOG.unlink()
|
|
40
|
+
print("[Sprint] Sprint log cleared.")
|
storage/streak.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from supabase import Client, create_client, ClientOptions
|
|
3
|
+
from config.settings import SUPABASE_SERVICE_KEY, SUPABASE_URL
|
|
4
|
+
|
|
5
|
+
_thread_local = threading.local()
|
|
6
|
+
|
|
7
|
+
def get_client() -> Client:
|
|
8
|
+
if not SUPABASE_URL or not SUPABASE_SERVICE_KEY:
|
|
9
|
+
raise RuntimeError("SUPABASE_URL and SUPABASE_SERVICE_KEY must be configured")
|
|
10
|
+
|
|
11
|
+
if not hasattr(_thread_local, "client"):
|
|
12
|
+
_thread_local.client = create_client(
|
|
13
|
+
SUPABASE_URL,
|
|
14
|
+
SUPABASE_SERVICE_KEY,
|
|
15
|
+
options=ClientOptions(
|
|
16
|
+
postgrest_client_timeout=10,
|
|
17
|
+
storage_client_timeout=10
|
|
18
|
+
)
|
|
19
|
+
)
|
|
20
|
+
return _thread_local.client
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
client = get_client()
|
|
25
|
+
print(f"[Supabase] Client initialized: {bool(client)}")
|
storage/tone_memory.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from config.settings import TONE_LOG
|
|
5
|
+
|
|
6
|
+
# [ToneMemory] module for RLHF-style feedback and few-shot example selection
|
|
7
|
+
|
|
8
|
+
def save_rating(post_text: str, format_key: str, rating: int, timestamp: str = ""):
|
|
9
|
+
"""Appends a post rating to TONE_LOG as newline-delimited JSON."""
|
|
10
|
+
if not timestamp:
|
|
11
|
+
timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
|
|
12
|
+
|
|
13
|
+
entry = {
|
|
14
|
+
"timestamp": timestamp,
|
|
15
|
+
"format_key": format_key,
|
|
16
|
+
"post_text": post_text,
|
|
17
|
+
"rating": rating,
|
|
18
|
+
"engagement": {
|
|
19
|
+
"likes": 0,
|
|
20
|
+
"retweets": 0,
|
|
21
|
+
"replies": 0,
|
|
22
|
+
"impressions": 0
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
with open(TONE_LOG, "a", encoding="utf-8") as f:
|
|
28
|
+
f.write(json.dumps(entry) + "\n")
|
|
29
|
+
print(f"[ToneMemory] Saved rating {rating} for {format_key}")
|
|
30
|
+
except Exception as e:
|
|
31
|
+
print(f"[ToneMemory] Error saving rating: {e}")
|
|
32
|
+
|
|
33
|
+
def load_ratings():
|
|
34
|
+
"""Returns all entries from TONE_LOG."""
|
|
35
|
+
if not TONE_LOG.exists():
|
|
36
|
+
return []
|
|
37
|
+
|
|
38
|
+
entries = []
|
|
39
|
+
try:
|
|
40
|
+
with open(TONE_LOG, "r", encoding="utf-8") as f:
|
|
41
|
+
for line in f:
|
|
42
|
+
if line.strip():
|
|
43
|
+
entries.append(json.loads(line))
|
|
44
|
+
except Exception as e:
|
|
45
|
+
print(f"[ToneMemory] Error loading ratings: {e}")
|
|
46
|
+
|
|
47
|
+
return entries
|
|
48
|
+
|
|
49
|
+
def get_few_shot_examples(format_key: str = None, top_n: int = 3) -> list[str]:
|
|
50
|
+
"""
|
|
51
|
+
Returns top_n post_text strings with highest rating for format_key.
|
|
52
|
+
If format_key is None, returns top examples across all formats.
|
|
53
|
+
"""
|
|
54
|
+
entries = load_ratings()
|
|
55
|
+
if not entries:
|
|
56
|
+
return []
|
|
57
|
+
|
|
58
|
+
if format_key:
|
|
59
|
+
filtered = [e for e in entries if e.get("format_key") == format_key]
|
|
60
|
+
else:
|
|
61
|
+
filtered = entries
|
|
62
|
+
|
|
63
|
+
# Sort by rating descending
|
|
64
|
+
sorted_entries = sorted(filtered, key=lambda x: x.get("rating", 0), reverse=True)
|
|
65
|
+
return [e["post_text"] for e in sorted_entries[:top_n]]
|
|
66
|
+
|
|
67
|
+
def update_engagement(post_text: str, likes: int, retweets: int, replies: int):
|
|
68
|
+
"""
|
|
69
|
+
Finds matching entry in TONE_LOG, updates with engagement metrics.
|
|
70
|
+
Higher engagement boosts effective rating weight.
|
|
71
|
+
"""
|
|
72
|
+
entries = load_ratings()
|
|
73
|
+
updated = False
|
|
74
|
+
|
|
75
|
+
# We use post_text as a loose identifier; in a real DB we'd use a UUID
|
|
76
|
+
for e in entries:
|
|
77
|
+
if e["post_text"] == post_text:
|
|
78
|
+
e["engagement"]["likes"] = likes
|
|
79
|
+
e["engagement"]["retweets"] = retweets
|
|
80
|
+
e["engagement"]["replies"] = replies
|
|
81
|
+
updated = True
|
|
82
|
+
break
|
|
83
|
+
|
|
84
|
+
if updated:
|
|
85
|
+
try:
|
|
86
|
+
with open(TONE_LOG, "w", encoding="utf-8") as f:
|
|
87
|
+
for e in entries:
|
|
88
|
+
f.write(json.dumps(e) + "\n")
|
|
89
|
+
print("[ToneMemory] Updated engagement metrics for post")
|
|
90
|
+
except Exception as e:
|
|
91
|
+
print(f"[ToneMemory] Error updating engagement: {e}")
|
|
92
|
+
|
|
93
|
+
def get_weighted_examples(format_key: str, top_n: int = 3) -> list[str]:
|
|
94
|
+
"""
|
|
95
|
+
Combines explicit rating + engagement metrics into weighted score.
|
|
96
|
+
Returns top_n examples by weighted score.
|
|
97
|
+
Score = rating + (likes * 0.5) + (retweets * 1.0) + (replies * 2.0)
|
|
98
|
+
"""
|
|
99
|
+
entries = load_ratings()
|
|
100
|
+
if not entries:
|
|
101
|
+
return []
|
|
102
|
+
|
|
103
|
+
filtered = [e for e in entries if e.get("format_key") == format_key]
|
|
104
|
+
|
|
105
|
+
def calculate_score(entry):
|
|
106
|
+
r = entry.get("rating", 0)
|
|
107
|
+
eng = entry.get("engagement", {})
|
|
108
|
+
score = r + (eng.get("likes", 0) * 0.5) + (eng.get("retweets", 0) * 1.0) + (eng.get("replies", 0) * 2.0)
|
|
109
|
+
return score
|
|
110
|
+
|
|
111
|
+
sorted_entries = sorted(filtered, key=calculate_score, reverse=True)
|
|
112
|
+
return [e["post_text"] for e in sorted_entries[:top_n]]
|
|
113
|
+
|
|
114
|
+
if __name__ == "__main__":
|
|
115
|
+
print("=== TONE MEMORY TEST ===")
|
|
116
|
+
|
|
117
|
+
# Clear and test
|
|
118
|
+
if TONE_LOG.exists():
|
|
119
|
+
TONE_LOG.unlink()
|
|
120
|
+
|
|
121
|
+
save_rating("Example post 1", "deep_tech", 5)
|
|
122
|
+
save_rating("Example post 2", "deep_tech", 3)
|
|
123
|
+
save_rating("Example post 3", "deep_tech", 4)
|
|
124
|
+
save_rating("Example post 4", "struggle", 5)
|
|
125
|
+
|
|
126
|
+
examples = get_few_shot_examples("deep_tech", top_n=2)
|
|
127
|
+
print(f"Top 2 Deep Tech examples: {examples}")
|
|
128
|
+
assert len(examples) == 2
|
|
129
|
+
assert examples[0] == "Example post 1"
|
|
130
|
+
|
|
131
|
+
# Test engagement boost
|
|
132
|
+
update_engagement("Example post 3", likes=10, retweets=5, replies=2)
|
|
133
|
+
weighted = get_weighted_examples("deep_tech", top_n=1)
|
|
134
|
+
print(f"Top weighted Deep Tech: {weighted}")
|
|
135
|
+
# Score for post 1: 5
|
|
136
|
+
# Score for post 3: 4 + (10*0.5) + (5*1.0) + (2*2.0) = 4 + 5 + 5 + 4 = 18
|
|
137
|
+
assert weighted[0] == "Example post 3"
|
|
138
|
+
|
|
139
|
+
print("Tone Memory tests: PASSED")
|
ui/__init__.py
ADDED
|
File without changes
|
web/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Make web directory a Python package so it gets packaged and installed to site-packages.
|