- Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtransform.py
More file actions
Latest commit
307 lines (250 loc) · 13.8 KB
/
Copy pathtransform.py
File metadata and controls
307 lines (250 loc) · 13.8 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
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
"""
Transformation Module: Handles preprocessing of diffs and messages, and interacts with the Gemini API
to generate blog posts, LinkedIn summaries, and click-worthy titles.
"""
importasyncio
importtime
importos
importgoogle.generativeaiasgenai
importlogging
fromtypingimportOptional
fromtenacityimportretry, wait_exponential, stop_after_attempt, retry_if_exception_type, before_log, after_log
# Configure logging for this module
logger=logging.getLogger(__name__)
classAsyncTokenRateLimiter:
"""
Manages API call rates for both TPM and RPM with a unified delay mechanism.
This is an ASYNCHRONOUS version.
"""
def__init__(self, capacity: int, refill_rate_per_minute: int, rpm_limit: int):
self.capacity=float(capacity)
self.tokens=float(capacity)
self.refill_rate_per_second=float(refill_rate_per_minute) /60.0
self.last_refill_time=time.monotonic()
self._lock=asyncio.Lock()
self._rpm_delay=60.0/rpm_limitifrpm_limit>0else0
def_refill(self):
"""Refills the token bucket based on the time elapsed since the last refill."""
now=time.monotonic()
elapsed=now-self.last_refill_time
self.tokens=min(self.capacity, self.tokens+elapsed*self.refill_rate_per_second)
self.last_refill_time=now
asyncdefconsume(self, tokens_to_consume: int, model_name: str):
"""
Asynchronously checks if a call can proceed based on token budget. If not, it waits.
This version checks the budget BEFORE the call is made to prevent 429 errors.
"""
asyncwithself._lock:
iftokens_to_consume>self.capacity:
logger.warning(f"Call to '{model_name}' requests {tokens_to_consume} tokens, which exceeds the bucket capacity of {self.capacity}.")
self._refill()
iftokens_to_consume>self.tokens:
required_tokens=tokens_to_consume-self.tokens
wait_time=required_tokens/self.refill_rate_per_second
logger.info(f"TPM Limit for {model_name}: Pausing for {wait_time:.2f}s to refill tokens.")
awaitasyncio.sleep(wait_time)
self._refill()
self.tokens-=tokens_to_consume
asyncdefenforce_rpm_delay(self):
"""Asynchronously enforces the mandatory delay between requests."""
ifself._rpm_delay>0:
awaitasyncio.sleep(self._rpm_delay)
classTransformer:
"""
Manages the transformation of raw commit/note data into publishable content
using the Gemini API.
"""
def__init__(self, gemini_api_key: str, model_configs: dict):
"""
Initializes the Transformer with API keys and model configurations.
Args:
gemini_api_key (str): Your Google AI Studio Gemini API key.
model_configs (dict): A dictionary containing model names and their rate limits.
"""
genai.configure(api_key=gemini_api_key)
self.models= {
'blog': genai.GenerativeModel(model_configs['blog']['name']),
'summary': genai.GenerativeModel(model_configs['summary']['name']),
'linkedin': genai.GenerativeModel(model_configs['linkedin']['name']),
'title': genai.GenerativeModel(model_configs['title']['name'])
}
self.rate_limiters= {}
unique_model_names= {cfg['name'] forcfginmodel_configs.values()}
formodel_nameinunique_model_names:
# Find the config for this unique model name
# This assumes model names in the config dict are consistent
config=next((cfgforcfginmodel_configs.values() ifcfg['name'] ==model_name), None)
ifconfig:
self.rate_limiters[model_name] =AsyncTokenRateLimiter(
capacity=config['tpm'],
refill_rate_per_minute=config['tpm'],
rpm_limit=config['rpm']
)
logger.info(f"Initialized rate limiter for {model_name}: {config['rpm']} RPM, {config['tpm']} TPM")
@retry(wait=wait_exponential(multiplier=1, min=4, max=10),
stop=stop_after_attempt(3),
retry=retry_if_exception_type(genai.types.BlockedPromptException),
before_sleep=before_log(logger, logging.INFO),
after=after_log(logger, logging.WARNING))
asyncdef_call_gemini_async(self, model_key: str, prompt: str) ->str:
"""
Helper function to call Gemini API asynchronously with per-model rate limiting.
"""
model=self.models[model_key]
model_name=model.model_name
# 1. Pre-flight token counting and rate limiting
ifmodel_nameinself.rate_limiters:
limiter=self.rate_limiters[model_name]
try:
# Count tokens before the main call
prompt_tokens=awaitmodel.count_tokens_async(prompt)
# Consume tokens from the bucket (may wait)
awaitlimiter.consume(prompt_tokens.total_tokens, model_name)
# Enforce a fixed delay for RPM
awaitlimiter.enforce_rpm_delay()
exceptExceptionase:
logger.error(f"Error during token counting or rate limiting for {model_name}: {e}")
# Fail safe, proceed with call but log warning
# 2. Make the actual API call
response=None
try:
logger.info(f"Calling Gemini ({model_name}) with prompt: {prompt[:100]}...")
response=awaitmodel.generate_content_async(prompt)
returnresponse.text
exceptValueError:
ifresponse:
logger.warning(f"Gemini returned no content. It may have been blocked. Prompt feedback: {response.prompt_feedback}")
else:
logger.warning("Gemini call failed with ValueError before a response object was created.")
return""
exceptExceptionase:
logger.error(f"Unexpected error during Gemini API call to {model_name}: {e}", exc_info=True)
return""
asyncdef_summarize_single_file_async(self, file: dict) ->str:
"""Asynchronously summarizes the diff for a single file."""
filename=file.get("filename", "Unknown File")
status=file.get("status", "")
patch=file.get("patch", "")
ifnotpatch:
returnf"File: {filename} ({status}) - No patch content available."
iflen(patch) >1000: # Arbitrary threshold for large diffs
logger.info(f"Summarizing large diff for {filename} using Gemini.")
summary_prompt=f"Summarize the following code diff for file {filename} ({status}):\n```\n{patch}\n```\nProvide a concise summary focusing on the key changes and their purpose."
diff_summary_text=awaitself._call_gemini_async('summary', summary_prompt)
returnf"File: {filename} ({status})\nSummary: {diff_summary_text}"
else:
lines=patch.split("\n")
relevant_lines= [lineforlineinlinesifline.startswith(("+", "-")) andnotline.startswith(("+++", "---"))]
summary="\n".join(relevant_lines[:10]) + ("..."iflen(relevant_lines) >10else"")
returnf"File: {filename} ({status})\n{summary}"
asyncdef_summarize_diff_async(self, files_changed: list) ->str:
"""
Summarizes code changes from diffs, running large diff summaries concurrently.
"""
ifnotfiles_changed:
return"No significant code changes detected."
tasks= [self._summarize_single_file_async(file) forfileinfiles_changed]
summaries=awaitasyncio.gather(*tasks)
return"\n\n".join(summaries)
asyncdefgenerate_blog_post(self, commit_message: str, files_changed: list, notion_content: str="", aggregated_context: str="") ->str:
"""
Generates a Markdown-formatted blog post based on commit message, diff, previous posts and Notion content.
Args:
commit_message (str): The main commit message.
files_changed (list): List of changed files with diff patches.
notion_content (str): Optional, relevant content from Notion notes.
Returns:
str: The generated blog post in Markdown format.
"""
diff_summary=awaitself._summarize_diff_async(files_changed)
context_prompt_part=""
ifaggregated_context:
context_prompt_part=f"""
**Previous Blog Posts (for context and to avoid repetition):**
---
{aggregated_context}
---
"""
prompt=f"""Write a technical blog post in Markdown format based on the information below. The tone should be that of an engineer explaining their own project — first-person, direct, and focused on the "why" behind the work.
{context_prompt_part}
High-Level Context (from Notion note):
{notion_contentifnotion_content.strip() else"Not provided — infer the purpose from the commit message and code changes below."}
Commit Message:
{commit_message}
Code Changes Summary:
{diff_summary}
Instructions:
1. Use the Notion note as the narrative foundation if provided; otherwise lead with the commit context.
2. Weave in the technical details from the code changes to support the story — don't just list them.
3. Explain the impact and reasoning behind the update.
4. Keep the post distinct from the previous posts — do not repeat existing content.
5. Structure it with a clear introduction, body, and conclusion. Use headings and lists for readability.
"""
logger.info("Generating blog post with Gemini...")
returnawaitself._call_gemini_async('blog', prompt)
asyncdefgenerate_linkedin_summary(self, commit_message: str, files_changed: list, notion_content: str="") ->str:
"""
Generates a concise LinkedIn-friendly summary of the changes.
"""
diff_summary=awaitself._summarize_diff_async(files_changed)
prompt=f"""You are a professional content creator for LinkedIn.
Based on the following technical update, craft a concise (100-150 words) and impactful summary for a LinkedIn post.
Focus on the value and impact of the changes, suitable for a professional network.
**Commit Message:**
{commit_message}
**Code Changes Summary:**
{diff_summary}
**Additional Context (from Notion notes, if any):**
{notion_content}
Include relevant keywords and a call to action if appropriate (e.g., "Learn more in my latest blog post").
"""
logger.info("Generating LinkedIn summary with Gemini...")
returnawaitself._call_gemini_async('linkedin', prompt)
asyncdefgenerate_click_worthy_title(self, blog_post_content: str, commit_message: Optional[str] =None ) ->str:
"""
Generates a click-worthy and SEO-friendly title for the blog post.
"""
prompt=f"""You are an expert in SEO and content marketing.
Based on the following commit message and blog post content, generate 3-5 highly click-worthy and SEO-friendly titles.
Prioritize titles that are engaging, informative, and include relevant keywords.
**Commit Message:**
{commit_messageifcommit_messageelse"No commit message provided."}
**Blog Post Content (for context):**
{blog_post_content}
Provide only the titles, one per line, without any additional text or numbering.
"""
logger.info("Generating click-worthy titles with Gemini...")
response_text=awaitself._call_gemini_async('title', prompt)
titles= [t.strip() fortinresponse_text.split("\n") ift.strip()]
returntitles[0] iftitleselse"Default Blog Post Title"
if__name__=='__main__':
GEMINI_API_KEY=os.getenv("GEMINI_API_KEY")
ifnotGEMINI_API_KEY:
print("Please set the GEMINI_API_KEY environment variable.")
else:
asyncdefmain():
transformer=Transformer(gemini_api_key=GEMINI_API_KEY) # type: ignore
sample_commit_message="feat: Add user authentication with OAuth2"
sample_files_changed= [
{
"filename": "auth.py",
"status": "added",
"patch": """--- /dev/null\n+++ b/auth.py\n@@ -0,0 +1,20 @@\n+import oauthlib\n+from flask import Flask, redirect, url_for, session, request\n+# ... (more code)\n+def login():\n+ # OAuth2 flow\n+ pass\n+"""
},
{
"filename": "app.py",
"status": "modified",
"patch": """--- a/app.py\n+++ b/app.py\n@@ -10,6 +10,7 @@\n from . import db\n from .auth import login_required, login\n\n+app.register_blueprint(auth.bp)\n @app.route("/hello")\n def hello():\n return "Hello, World!"\n"""
}
]
sample_notion_content="Design notes: OAuth2 integration for user login. Use Google as provider."
# Generate blog post
blog_post=awaittransformer.generate_blog_post(sample_commit_message, sample_files_changed, sample_notion_content)
print("\n--- Generated Blog Post ---\n", blog_post)
# Generate LinkedIn summary
linkedin_summary=awaittransformer.generate_linkedin_summary(sample_commit_message, sample_files_changed, sample_notion_content)
print("\n--- Generated LinkedIn Summary ---\n", linkedin_summary)
# Generate click-worthy title
title=awaittransformer.generate_click_worthy_title(sample_commit_message, blog_post)
print("\n--- Generated Title ---\n", title)
asyncio.run(main())