@@ -149,6 +149,7 @@ def cost_explorer_url(start, end):
149149
150150MAX_SERVICES_PER_CATEGORY = 8
151151SPIKE_THRESHOLD_PCT = 15.0
152+ USD_TO_NOK = 10.5 # Approximate exchange rate
152153
153154
154155# ---------------------------------------------------------------------------
@@ -158,6 +159,12 @@ def fmt_date(d):
158159 return d .strftime ("%b %d" )
159160
160161
162+ def fmt_usd_nok (usd ):
163+ """Format amount in both USD and NOK."""
164+ nok = usd * USD_TO_NOK
165+ return f"${ usd :.2f} (~{ nok :.0f} NOK)"
166+
167+
161168def fmt_service_lines (services , start , end , week_total ):
162169 """Return sorted mrkdwn lines with % of total."""
163170 url = cost_explorer_url (start , end )
@@ -193,81 +200,76 @@ def fmt_change(current, previous):
193200# LLM narrative generation via Bedrock — structured output
194201# ---------------------------------------------------------------------------
195202_NARRATIVE_TOOL = {
196- "tools" : [{
197- "toolSpec" : {
198- "name" : "submit_narrative" ,
199- "description" : "Submit a cost report narrative summary" ,
200- "inputSchema" : {
201- "json" : {
202- "type" : "object" ,
203- "properties" : {
204- "summary" : {
205- "type" : "string" ,
206- "description" : "2-3 sentence executive summary of the cost report" ,
207- },
208- "notable" : {
209- "type" : "boolean" ,
210- "description" : "True if there is something worth calling out "
211- "(significant change, unusual pattern). False if costs are stable." ,
212- },
213- },
214- "required" : ["summary" , "notable" ],
215- }
203+ "name" : "submit_narrative" ,
204+ "description" : "Submit a cost report narrative summary" ,
205+ "input_schema" : {
206+ "type" : "object" ,
207+ "properties" : {
208+ "summary" : {
209+ "type" : "string" ,
210+ "description" : "2-3 sentence executive summary of the cost report" ,
216211 },
217- }
218- }],
219- "toolChoice" : {"any" : {}},
212+ "notable" : {
213+ "type" : "boolean" ,
214+ "description" : "True if there is something worth calling out "
215+ "(significant change, unusual pattern). False if costs are stable." ,
216+ },
217+ },
218+ "required" : ["summary" , "notable" ],
219+ },
220220}
221221
222222_SPIKE_TOOL = {
223- "tools" : [{
224- "toolSpec" : {
225- "name" : "submit_spike_analysis" ,
226- "description" : "Submit a root cause analysis for a cost spike" ,
227- "inputSchema" : {
228- "json" : {
229- "type" : "object" ,
230- "properties" : {
231- "root_cause" : {
232- "type" : "string" ,
233- "description" : "2-3 sentence explanation of the likely root cause" ,
234- },
235- "suggested_action" : {
236- "type" : "string" ,
237- "description" : "One concrete action to investigate" ,
238- },
239- "severity" : {
240- "type" : "string" ,
241- "enum" : ["LOW" , "MEDIUM" , "HIGH" ],
242- "description" : "LOW = expected growth, MEDIUM = worth investigating, "
243- "HIGH = likely anomaly needing immediate attention" ,
244- },
245- },
246- "required" : ["root_cause" , "suggested_action" , "severity" ],
247- }
223+ "name" : "submit_spike_analysis" ,
224+ "description" : "Submit a root cause analysis for a cost spike" ,
225+ "input_schema" : {
226+ "type" : "object" ,
227+ "properties" : {
228+ "root_cause" : {
229+ "type" : "string" ,
230+ "description" : "2-3 sentence explanation of the likely root cause" ,
231+ },
232+ "suggested_action" : {
233+ "type" : "string" ,
234+ "description" : "One concrete action to investigate" ,
248235 },
249- }
250- }],
251- "toolChoice" : {"any" : {}},
236+ "severity" : {
237+ "type" : "string" ,
238+ "enum" : ["LOW" , "MEDIUM" , "HIGH" ],
239+ "description" : "LOW = expected growth, MEDIUM = worth investigating, "
240+ "HIGH = likely anomaly needing immediate attention" ,
241+ },
242+ },
243+ "required" : ["root_cause" , "suggested_action" , "severity" ],
244+ },
252245}
253246
254247
255- def _llm_structured (prompt , tool_config ):
256- """Call Bedrock with tool use, return structured dict or None."""
248+ def _llm_structured (prompt , tool ):
249+ """Call Bedrock invoke_model with tool use, return structured dict or None."""
257250 try :
258251 bedrock = boto3 .client (
259252 "bedrock-runtime" , region_name = LLM_REGION ,
260253 config = Config (read_timeout = 20 , connect_timeout = 5 ),
261254 )
262- response = bedrock .converse (
255+ request_body = json .dumps ({
256+ "anthropic_version" : "bedrock-2023-05-31" ,
257+ "max_tokens" : 1024 ,
258+ "temperature" : 0.2 ,
259+ "messages" : [{"role" : "user" , "content" : prompt }],
260+ "tools" : [tool ],
261+ "tool_choice" : {"type" : "any" },
262+ })
263+ response = bedrock .invoke_model (
263264 modelId = LLM_MODEL_ID ,
264- messages = [{ "role" : "user" , "content" : [{ "text" : prompt }]}] ,
265- toolConfig = tool_config ,
266- inferenceConfig = { "temperature" : 0.2 } ,
265+ contentType = "application/json" ,
266+ accept = "application/json" ,
267+ body = request_body ,
267268 )
268- for content in response ["output" ]["message" ]["content" ]:
269- if "toolUse" in content :
270- return content ["toolUse" ]["input" ]
269+ response_body = json .loads (response ["body" ].read ())
270+ for block in response_body .get ("content" , []):
271+ if block .get ("type" ) == "tool_use" and block .get ("name" ) == tool ["name" ]:
272+ return block ["input" ]
271273 except Exception as e :
272274 logger .warning ("LLM call failed: %s" , e )
273275 return None
@@ -370,7 +372,8 @@ def build_blocks(this_week, prev_week, mtd, prev_mtd, project_costs,
370372 for proj_name , cost in sorted_projects :
371373 if cost >= 0.01 :
372374 pct = (cost / tw_total * 100 ) if tw_total > 0 else 0
373- project_lines .append (f" * { proj_name } — *${ cost :.2f} * ({ pct :.1f} %)" )
375+ nok = cost * USD_TO_NOK
376+ project_lines .append (f" \u2022 { proj_name } \u2014 *${ cost :.2f} * (~{ nok :.0f} NOK) `{ pct :.1f} %`" )
374377 if project_lines :
375378 blocks .append ({
376379 "type" : "section" ,
@@ -409,12 +412,12 @@ def build_blocks(this_week, prev_week, mtd, prev_mtd, project_costs,
409412 blocks .append ({
410413 "type" : "section" ,
411414 "fields" : [
412- {"type" : "mrkdwn" , "text" : f"*Total This Week*\n $ { tw_total :.2f } " },
413- {"type" : "mrkdwn" , "text" : f"*Previous Week*\n $ { pw_total :.2f } " },
415+ {"type" : "mrkdwn" , "text" : f"*Total This Week*\n { fmt_usd_nok ( tw_total ) } " },
416+ {"type" : "mrkdwn" , "text" : f"*Previous Week*\n { fmt_usd_nok ( pw_total ) } " },
414417 {"type" : "mrkdwn" , "text" : f"*Week-over-Week*\n { fmt_change (tw_total , pw_total )} " },
415- {"type" : "mrkdwn" , "text" : f"*Avg Daily*\n $ { daily_avg :.2f } " },
416- {"type" : "mrkdwn" , "text" : f"*Month-to-Date*\n $ { mtd_total :.2f } " },
417- {"type" : "mrkdwn" , "text" : f"*Projected { curr_month_name } *\n $ { projected :.2f } " },
418+ {"type" : "mrkdwn" , "text" : f"*Avg Daily*\n { fmt_usd_nok ( daily_avg ) } " },
419+ {"type" : "mrkdwn" , "text" : f"*Month-to-Date*\n { fmt_usd_nok ( mtd_total ) } " },
420+ {"type" : "mrkdwn" , "text" : f"*Projected { curr_month_name } *\n { fmt_usd_nok ( projected ) } " },
418421 ]
419422 })
420423
@@ -429,28 +432,16 @@ def build_blocks(this_week, prev_week, mtd, prev_mtd, project_costs,
429432 blocks .append ({
430433 "type" : "section" ,
431434 "fields" : [
432- {"type" : "mrkdwn" , "text" : f"*MTD Total*\n $ { mtd_total :.2f } " },
433- {"type" : "mrkdwn" , "text" : f"*{ prev_month_name } Same Period*\n $ { prev_mtd_total :.2f } " },
435+ {"type" : "mrkdwn" , "text" : f"*MTD Total*\n { fmt_usd_nok ( mtd_total ) } " },
436+ {"type" : "mrkdwn" , "text" : f"*{ prev_month_name } Same Period*\n { fmt_usd_nok ( prev_mtd_total ) } " },
434437 {"type" : "mrkdwn" , "text" : f"*MTD Change*\n { fmt_change (mtd_total , prev_mtd_total )} " },
435- {"type" : "mrkdwn" , "text" : f"*Projected { curr_month_name } *\n $ { projected :.2f } " },
438+ {"type" : "mrkdwn" , "text" : f"*Projected { curr_month_name } *\n { fmt_usd_nok ( projected ) } " },
436439 ]
437440 })
438441
439442 blocks .append ({"type" : "divider" })
440443
441- # LLM narrative
442- narrative_result = generate_narrative (
443- this_week , prev_week , mtd ,
444- tw_total , pw_total , mtd_total , projected , curr_month_name ,
445- )
446- if narrative_result :
447- blocks .append ({
448- "type" : "section" ,
449- "text" : {"type" : "mrkdwn" ,
450- "text" : f":brain: *Summary*\n { narrative_result .get ('summary' , '' )} " }
451- })
452-
453- # LLM spike root cause
444+ # LLM spike root cause (shown as a section — important)
454445 spike_result = analyze_spike (this_week , prev_week , tw_total , pw_total )
455446 if spike_result :
456447 sev_emoji = {"HIGH" : ":red_circle:" , "MEDIUM" : ":large_orange_circle:" }.get (
@@ -461,18 +452,25 @@ def build_blocks(this_week, prev_week, mtd, prev_mtd, project_costs,
461452 "text" : f":warning: *Spike Analysis* { sev_emoji } \n { spike_result .get ('root_cause' , '' )} \n \n "
462453 f"_Suggested action: { spike_result .get ('suggested_action' , 'N/A' )} _" }
463454 })
464-
465- if narrative_result or spike_result :
466455 blocks .append ({"type" : "divider" })
467456
457+ # LLM narrative + footer as context blocks
458+ narrative_result = generate_narrative (
459+ this_week , prev_week , mtd ,
460+ tw_total , pw_total , mtd_total , projected , curr_month_name ,
461+ )
462+
463+ ce_url = cost_explorer_url (tw_start , tw_end )
468464 ts = datetime .now (timezone .utc ).strftime ("%Y-%m-%d %H:%M UTC" )
469- blocks .append ({
470- "type" : "context" ,
471- "elements" : [
472- {"type" : "mrkdwn" ,
473- "text" : f"Source: AWS Cost Explorer | Generated { ts } " }
474- ]
475- })
465+ context_parts = []
466+ if narrative_result :
467+ context_parts .append (
468+ {"type" : "mrkdwn" , "text" : f":brain: { narrative_result .get ('summary' , '' )} " }
469+ )
470+ context_parts .append (
471+ {"type" : "mrkdwn" , "text" : f"<{ ce_url } |View in Cost Explorer> | Generated { ts } | NOK rate: ~{ USD_TO_NOK } " }
472+ )
473+ blocks .append ({"type" : "context" , "elements" : context_parts })
476474
477475 return blocks
478476
0 commit comments