Derfel2025 commited on
Commit
d8fea2b
·
1 Parent(s): b63b702

updated space to allow prompt to be passed down, as well as general updates

Browse files
Files changed (2) hide show
  1. app.py +429 -97
  2. appOld.py +789 -0
app.py CHANGED
@@ -3,9 +3,10 @@
3
  #do autonomous llamagents
4
 
5
  from llama_index.core.tools import FunctionTool
6
- from llama_index.llms.openai import OpenAI
7
  from dotenv import load_dotenv
8
- #from llama_index.llms.huggingface_api import HuggingFaceInferenceAPI
 
9
  from llama_index.core.agent.workflow import AgentWorkflow, FunctionAgent, ReActAgent #can also import ReActAgent or FunctionAgent from this
10
  from llama_index.core.tools import FunctionTool
11
  from llama_index.core.workflow import Context
@@ -25,7 +26,7 @@ import tiktoken
25
  import requests
26
  import json
27
  import gradio as gr
28
-
29
 
30
 
31
  #from llama_index.llms.google_gemini import GoogleGenAI
@@ -34,13 +35,32 @@ import gradio as gr
34
  load_dotenv()
35
 
36
  #llm = OpenAI(model="gpt-4o-mini")
 
 
 
 
 
 
37
 
38
- llm = OpenAI(
 
 
 
39
  model="gpt-4o-mini", # or "gpt-3.5-turbo"
40
  api_key=os.getenv('OPENAI_API_KEY'), # You can also set this via the OPENAI_API_KEY environment variable
41
  streaming=True
42
  )
43
 
 
 
 
 
 
 
 
 
 
 
44
  openai.api_key = os.getenv("OPENAI_API_KEY")
45
  #use gemini
46
 
@@ -108,7 +128,8 @@ async def find_artist_id_for_artist(ctx: Context, artist_name: str) -> int:
108
 
109
 
110
  """
111
- current_state = await ctx.get('state')
 
112
 
113
  access_token = get_chartmetric_access_token_cached()
114
 
@@ -140,8 +161,10 @@ async def find_artist_id_for_artist(ctx: Context, artist_name: str) -> int:
140
  if "working_notes" not in current_state:
141
  current_state["working_notes"] = {}
142
 
143
- current_state["working_notes"][artist_name] = artist_id
144
- await ctx.set("state", current_state) # 🟢 Save the updated state
 
 
145
 
146
  return artist_id
147
 
@@ -163,7 +186,8 @@ async def get_similar_artists(ctx: Context, artist_id: int) -> dict:
163
  Notes:
164
  - Results are stored in working memory under "similar_artists".
165
  """
166
- current_state = await ctx.get('state')
 
167
 
168
  access_token = get_chartmetric_access_token_cached() # Assuming this is defined elsewhere
169
  print("access_token for get_similar_artists api call obatined!")
@@ -188,7 +212,7 @@ async def get_similar_artists(ctx: Context, artist_id: int) -> dict:
188
  current_state["working_notes"] = {}
189
 
190
  current_state["working_notes"]["similar_artists"] = similar_artists
191
- await ctx.set('state', current_state)
192
 
193
  return similar_artists
194
 
@@ -210,7 +234,8 @@ async def get_youtube_audience_data(ctx: Context, artist_id: str) -> dict:
210
  Notes:
211
  - Results are saved in working memory.
212
  """
213
- current_state = await ctx.get('state')
 
214
 
215
  access_token = get_chartmetric_access_token_cached()
216
 
@@ -264,7 +289,7 @@ async def get_youtube_audience_data(ctx: Context, artist_id: str) -> dict:
264
  youtube_audience_stats = dict_to_return
265
  print(f"youtube_audience_stats are: {youtube_audience_stats}")
266
  current_state["working_notes"][f"youtube_audience_data for artist {artist_id}"] = youtube_audience_stats
267
- await ctx.set('state', current_state)
268
 
269
  return { f"youtube_audience_data for artist {artist_id}": youtube_audience_stats}
270
 
@@ -282,12 +307,13 @@ async def get_tiktok_audience_data(ctx: Context, artist_id: str) -> dict:
282
  - artist_id (str): The Chartmetric artist ID.
283
 
284
  Returns:
285
- - dict: Instagram audience breakdown.
286
 
287
  Notes:
288
  - Results are saved in working memory.
289
  """
290
- current_state = await ctx.get('state')
 
291
 
292
  access_token = get_chartmetric_access_token_cached()
293
 
@@ -333,9 +359,9 @@ async def get_tiktok_audience_data(ctx: Context, artist_id: str) -> dict:
333
  current_state["working_notes"] = {}
334
 
335
  tiktok_audience_stats = dict_to_return
336
- print(f"tiktok_audience_data are: {tiktok_audience_stats}")
337
  current_state["working_notes"][f"tiktok_audience_data for artist {artist_id}"] = tiktok_audience_stats
338
- await ctx.set('state', current_state)
339
 
340
  return { f"tiktok_audience_data for artist {artist_id}": tiktok_audience_stats}
341
 
@@ -363,7 +389,8 @@ async def get_instagram_audience_data(ctx: Context, artist_id: str) -> dict:
363
  #perhaps just have it get access_token inside here
364
  #access_token = get_chartmetric_access_token_with_refresh()
365
 
366
- current_state = await ctx.get('state')
 
367
 
368
  access_token = get_chartmetric_access_token_cached()
369
 
@@ -382,8 +409,8 @@ async def get_instagram_audience_data(ctx: Context, artist_id: str) -> dict:
382
  raise Exception(f"API request failed: {response.status_code} {response.reason}")
383
 
384
  data = response.json()
385
- print(f"data from api call is: {data}")
386
- print("Info from platform Instagram is:", data.get("obj"))
387
 
388
 
389
  if "working_notes" not in current_state:
@@ -391,12 +418,104 @@ async def get_instagram_audience_data(ctx: Context, artist_id: str) -> dict:
391
 
392
  instagram_audience_stats = data.get('obj', {})
393
  current_state["working_notes"][f"instagram_audience_data for artist {artist_id}"] = instagram_audience_stats
394
- await ctx.set('state', current_state)
395
 
396
  return { f"instagram_audience_data for artist {artist_id}": instagram_audience_stats}
397
 
398
 
399
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
 
401
 
402
 
@@ -413,15 +532,31 @@ async def get_instagram_audience_data(ctx: Context, artist_id: str) -> dict:
413
  #find_artist_id_for_artist_tool = FunctionTool(fn=find_artist_id_for_artist)
414
  #get_instagram_audience_stats_tool = FunctionTool(fn=get_instagram_audience_stats)
415
  #get_similar_artists_tool = FunctionTool(fn=get_similar_artists)
 
416
  manager_agent = ReActAgent(
417
  name="ManagerAgent",
418
  description="Manager agent decides which other agents to use, and is decision maker",
419
- system_prompt=("You are the manager agent, you decide which other agents to use and hand-off to."
420
- "You decide when the answer is ready to be returned to the user"
421
- ),
422
- can_handoff_to=["SocialMediaDataAgent", "SimilarityAgent"]
 
 
 
 
 
 
 
423
  )
424
 
 
 
 
 
 
 
 
 
425
 
426
 
427
  social_media_data_agent = ReActAgent(#try with Function Agents first, change to ReAct agents if needed/performance is poor.
@@ -437,9 +572,18 @@ social_media_data_agent = ReActAgent(#try with Function Agents first, change to
437
  "- Your tools are only for Instagram and TikTok and Youtube data.\n"
438
  )
439
  ,
440
- llm=llm,
441
  tools=[get_instagram_audience_data, find_artist_id_for_artist, get_tiktok_audience_data, get_youtube_audience_data],
442
- can_handoff_to=["ManagerAgent", "SimilarityAgent"]#allow it to handoff to all other agents
 
 
 
 
 
 
 
 
 
443
  )
444
 
445
  similarity_agent = ReActAgent(
@@ -450,7 +594,7 @@ similarity_agent = ReActAgent(
450
  ),
451
  llm=llm,
452
  tools=[get_similar_artists, find_artist_id_for_artist],
453
- can_handoff_to=["ManagerAgent", "SocialMediaDataAgent"]
454
  )
455
 
456
 
@@ -459,7 +603,7 @@ similarity_agent = ReActAgent(
459
 
460
 
461
 
462
- async def main(chosen_artist):
463
  #response = await workflow.run(user_msg="What is Bertie Blackman's Chartmetric artist ID?"
464
  #, ctx=ctx) python llamaOaAgent.py
465
  #chosen_artist = "Kenan Doğulu"
@@ -473,11 +617,14 @@ async def main(chosen_artist):
473
  f"Who are the most similar artists to {chosen_artist}? Based on sonic qualities and existing audience data, who are his closest peers?",
474
  f"Who is listening to artists similar to {chosen_artist}? What does the audience profile of {chosen_artist}'s peer artists look like, and where does it overlap with his?",
475
  f"Where can {chosen_artist} find new listeners? Which specific playlists (on Spotify, Apple Music, etc.) are crucial for reaching the fans of these similar artists?",
476
- f"Who should be {chosen_artist}'s audience? Based on all the available data, what does the ideal 'extended audience' look like that we should be targeting?"
 
477
  ]
478
  overall_answers = ""
479
  overall_answers2 = {}
480
 
 
 
481
  for (index, user_msg) in enumerate(questions):
482
 
483
  print(f"starting questions {index + 1}")
@@ -486,7 +633,7 @@ async def main(chosen_artist):
486
 
487
  #create/re-create workflow with new question as user_msg
488
  workflow = AgentWorkflow(
489
- agents=[similarity_agent, social_media_data_agent, manager_agent],
490
  root_agent=manager_agent.name,
491
  initial_state={"working_notes": {}, "user question": user_msg, "users language": "English"}
492
  )
@@ -494,6 +641,9 @@ async def main(chosen_artist):
494
  # run the workflow with context
495
  ctx = Context(workflow)
496
 
 
 
 
497
  handler = workflow.run(user_msg=user_msg, ctx=ctx)
498
  current_agent = None
499
  current_tool_calls = ""
@@ -551,7 +701,18 @@ async def main(chosen_artist):
551
  print(f"🔨 Calling Tool: {event.tool_name}")
552
  print(f" With arguments: {event.tool_kwargs}")
553
 
554
- print(f"overall_answers is: {overall_answers}")
 
 
 
 
 
 
 
 
 
 
 
555
 
556
  #can then keep just the last thought of each question index
557
 
@@ -572,10 +733,10 @@ async def main(chosen_artist):
572
  total_tokens2 = count_tokens(flattened)
573
  print(f"Total tokens of overall_answers2: {total_tokens2}")
574
 
575
- with open(f"overall_answers.txt","w", encoding="utf-8") as file:
576
  file.write(overall_answers)
577
 
578
- with open(f"overall_answers2.txt","w", encoding="utf-8") as file:
579
  json.dump(overall_answers2, file, ensure_ascii=False, indent=2)
580
 
581
 
@@ -621,68 +782,77 @@ RAW DATA:
621
  """
622
 
623
  prompto3 = f"""
624
- You are a senior music strategist preparing a 2-page audience intelligence brief for artist **{chosen_artist}**.
625
-
626
- You have been given raw data from Instagram, TikTok, Chartmetric, and related tools.
627
-
628
- ---
629
-
630
- 🎯 INSTRUCTIONS:
631
-
632
- - Use the detailed markdown template below as a **shell to be fully populated**.
633
- - **Do not repeat the template unfilled.**
634
- - Extract ALL relevant statistics, insights, and named entities from the raw data and synthesize them into this format.
635
- - If a section is partially unsupported (e.g., missing TikTok data), write that clearly — but **still include the section fully**.
636
- - Match the **tone, voice, and layout** of a presentation-ready strategy deck: consultative, insight-rich, structured.
637
- - Use bullet points, tables, and formatting to ensure visual clarity.
638
-
639
- ---
640
-
641
- ### Deep‑Dive Audience Analysis for {chosen_artist}
642
- (Synthesising Instagram data + Turkish pop‑market context)
 
 
 
 
 
 
 
 
 
643
 
644
  ---
645
 
646
  1. **Audience Architecture at a Glance**
647
  | Layer | Instagram Data | TikTok/Other* | Strategic Takeaway |
648
- |--------------------|---------------------------|------------------------|-------------------------------------------|
649
- | Scale | | | |
650
- | Core Territory | | | |
651
- | Secondary Markets | | | |
652
- | Gender | | | |
653
- | Prime Age Band | | | |
654
 
655
  ---
656
 
657
  2. **Hidden Insights & Underserved Nuances**
658
- | Insight | Evidence | Why It Matters |
659
- |-----------------------------|-------------------------------------|------------------------------------------|
660
- | | | |
661
- | | | |
662
- | | | |
663
 
664
  ---
665
 
666
- 3. **Psychographic MicroSegments to Activate**
667
- | Segment Name | % Audience | Description | Ideal Touchpoint |
668
- |----------------------------|------------|---------------------------------------|------------------------------------------|
669
- | | | | |
670
- | | | | |
671
 
672
  ---
673
 
674
  4. **Content & Channel Implications**
675
- | Funnel Stage | Channel | Format Strategy |
676
- |----------------|--------------------|-----------------------------------------|
677
- | Discovery | | |
678
- | Consideration | | |
679
- | Community | | |
680
- | Conversion | | |
681
 
682
  ---
683
 
684
  5. **Monetisation & Partnership Levers**
685
- - (Example: Stadium performance + merch collab with Galatasaray)
686
  -
687
  -
688
  -
@@ -690,10 +860,10 @@ You have been given raw data from Instagram, TikTok, Chartmetric, and related to
690
  ---
691
 
692
  6. **Risks & Mitigations**
693
- | Risk | Impact | Mitigation |
694
- |-------------------------------|---------------------|-------------------------------------------|
695
- | | | |
696
- | | | |
697
 
698
  ---
699
 
@@ -704,23 +874,151 @@ You have been given raw data from Instagram, TikTok, Chartmetric, and related to
704
 
705
  ---
706
 
707
- 📦 RAW DATA:
708
  {overall_answers}
 
 
 
 
709
  """
710
 
 
 
711
 
 
 
712
 
 
 
713
 
 
714
 
715
- formal_questions = [
716
- {
717
- "section": "Core Audience",
718
- "questions": f"Who is {chosen_artist}'s core audience?",
719
- "features": "include table breakdown of core audience"
720
- }
721
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
722
 
723
- ##cut down Thought input, so only last one returned with Answer from
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
724
 
725
 
726
  #count tokens anyway, for later usage:
@@ -729,8 +1027,6 @@ You have been given raw data from Instagram, TikTok, Chartmetric, and related to
729
 
730
  max_tokens = 16384 - total_tokens - 200
731
 
732
- response = openai.chat.completions.create(
733
- model="gpt-3.5-turbo-0125", # or "o3-pro" if enabled
734
  messages=[
735
  {
736
  "role": "system",
@@ -738,15 +1034,51 @@ You have been given raw data from Instagram, TikTok, Chartmetric, and related to
738
  },
739
  {
740
  "role": "user",
741
- "content": prompto3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
742
  }
743
  ],
744
- temperature=0.2, # low temp helps avoid paraphrasing
745
- max_tokens=4096 # adjust based on your expected output size
746
- )
 
 
 
 
 
 
 
 
 
 
747
 
748
- print(response.choices[0].message.content)
749
- two_pager_document = response.choices[0].message.content
750
 
751
  #add generation of two_pager_part2
752
 
@@ -754,7 +1086,7 @@ You have been given raw data from Instagram, TikTok, Chartmetric, and related to
754
  #for question in formal_questions:
755
  print(f"overall_answer2 is {overall_answers2}")
756
 
757
- with open(f"{chosen_artist}o32.txt","w", encoding="utf-8") as file:
758
  file.write(two_pager_document)
759
 
760
  return two_pager_document
@@ -762,7 +1094,7 @@ You have been given raw data from Instagram, TikTok, Chartmetric, and related to
762
 
763
  demo = gr.Interface(
764
  fn=main,
765
- inputs="text",
766
  outputs="text",
767
  title="artist report generator",
768
  description="generate report for artist"
 
3
  #do autonomous llamagents
4
 
5
  from llama_index.core.tools import FunctionTool
6
+ from llama_index.llms.openai import OpenAI as LlamaOpenAI
7
  from dotenv import load_dotenv
8
+ from llama_index.llms.huggingface_api import HuggingFaceInferenceAPI
9
+ from llama_index.llms.google_genai import GoogleGenAI
10
  from llama_index.core.agent.workflow import AgentWorkflow, FunctionAgent, ReActAgent #can also import ReActAgent or FunctionAgent from this
11
  from llama_index.core.tools import FunctionTool
12
  from llama_index.core.workflow import Context
 
26
  import requests
27
  import json
28
  import gradio as gr
29
+ from openai import OpenAI
30
 
31
 
32
  #from llama_index.llms.google_gemini import GoogleGenAI
 
35
  load_dotenv()
36
 
37
  #llm = OpenAI(model="gpt-4o-mini")
38
+ import google.generativeai as genai
39
+
40
+ #genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
41
+
42
+ #llmGeminiPro = GoogleGenAI(model="gemini-2.5-pro")
43
+ #print("llmGeminiPro loaded!")
44
 
45
+ #llmGeminiFlash = GoogleGenAI(model="gemini-2.5-flash")
46
+ #print("llmGeminiFlash loaded!")
47
+
48
+ llm = LlamaOpenAI(
49
  model="gpt-4o-mini", # or "gpt-3.5-turbo"
50
  api_key=os.getenv('OPENAI_API_KEY'), # You can also set this via the OPENAI_API_KEY environment variable
51
  streaming=True
52
  )
53
 
54
+ llmHigher = LlamaOpenAI(
55
+ model="o3",
56
+ api_key=os.getenv('OPENAI_API_KEY'),
57
+ streaming=True
58
+ )
59
+
60
+ client = OpenAI(
61
+ api_key=os.getenv('OPENAI_API_KEY'),
62
+ )
63
+
64
  openai.api_key = os.getenv("OPENAI_API_KEY")
65
  #use gemini
66
 
 
128
 
129
 
130
  """
131
+ current_state = await ctx.store.get('state')
132
+ print(f"value of current_state on load inside of find_artist_id_for_artist is: {current_state}")
133
 
134
  access_token = get_chartmetric_access_token_cached()
135
 
 
161
  if "working_notes" not in current_state:
162
  current_state["working_notes"] = {}
163
 
164
+ current_state["working_notes"][f"artist_id_for_{artist_name}"] = artist_id
165
+ await ctx.store.set("state", current_state) # 🟢 Save the updated state
166
+ print(f"🧠 Updated working_notes in find_artist_id_for_artist: {json.dumps(current_state['working_notes'], indent=2)}")
167
+
168
 
169
  return artist_id
170
 
 
186
  Notes:
187
  - Results are stored in working memory under "similar_artists".
188
  """
189
+ current_state = await ctx.store.get('state')
190
+ print(f"value of current_state on load inside of get_similar_artists is: {current_state}")
191
 
192
  access_token = get_chartmetric_access_token_cached() # Assuming this is defined elsewhere
193
  print("access_token for get_similar_artists api call obatined!")
 
212
  current_state["working_notes"] = {}
213
 
214
  current_state["working_notes"]["similar_artists"] = similar_artists
215
+ await ctx.store.set('state', current_state)
216
 
217
  return similar_artists
218
 
 
234
  Notes:
235
  - Results are saved in working memory.
236
  """
237
+ current_state = await ctx.store.get('state')
238
+ print(f"value of current_state on load inside of get_youtube_audience_data is: {current_state}")
239
 
240
  access_token = get_chartmetric_access_token_cached()
241
 
 
289
  youtube_audience_stats = dict_to_return
290
  print(f"youtube_audience_stats are: {youtube_audience_stats}")
291
  current_state["working_notes"][f"youtube_audience_data for artist {artist_id}"] = youtube_audience_stats
292
+ await ctx.store.set('state', current_state)
293
 
294
  return { f"youtube_audience_data for artist {artist_id}": youtube_audience_stats}
295
 
 
307
  - artist_id (str): The Chartmetric artist ID.
308
 
309
  Returns:
310
+ - dict: TikTok audience breakdown.
311
 
312
  Notes:
313
  - Results are saved in working memory.
314
  """
315
+ current_state = await ctx.store.get('state')
316
+ print(f"value of current_state on load inside of get_tiktok_audience_data is: {current_state}")
317
 
318
  access_token = get_chartmetric_access_token_cached()
319
 
 
359
  current_state["working_notes"] = {}
360
 
361
  tiktok_audience_stats = dict_to_return
362
+ #print(f"tiktok_audience_data are: {tiktok_audience_stats}")
363
  current_state["working_notes"][f"tiktok_audience_data for artist {artist_id}"] = tiktok_audience_stats
364
+ await ctx.store.set('state', current_state)
365
 
366
  return { f"tiktok_audience_data for artist {artist_id}": tiktok_audience_stats}
367
 
 
389
  #perhaps just have it get access_token inside here
390
  #access_token = get_chartmetric_access_token_with_refresh()
391
 
392
+ current_state = await ctx.store.get('state')
393
+ print(f"value of current_state on load inside of get_instagram_audience_stats is: {current_state}")
394
 
395
  access_token = get_chartmetric_access_token_cached()
396
 
 
409
  raise Exception(f"API request failed: {response.status_code} {response.reason}")
410
 
411
  data = response.json()
412
+ #print(f"data from api call is: {data}")
413
+ #print("Info from platform Instagram is:", data.get("obj"))
414
 
415
 
416
  if "working_notes" not in current_state:
 
418
 
419
  instagram_audience_stats = data.get('obj', {})
420
  current_state["working_notes"][f"instagram_audience_data for artist {artist_id}"] = instagram_audience_stats
421
+ await ctx.store.set('state', current_state)
422
 
423
  return { f"instagram_audience_data for artist {artist_id}": instagram_audience_stats}
424
 
425
 
426
 
427
+ async def get_charts(ctx: Context, artist_id: int, chart_type: str) -> dict:
428
+ """
429
+ Retrieve chart data for a given artist using Chartmetric API.
430
+
431
+ Parameters:
432
+ - artist_id (str): The Chartmetric artist ID.
433
+ - chart_type: The platform chart and sub-choice. Choose one from:
434
+ [
435
+ "spotify_viral_daily", "spotify_viral_weekly", "spotify_top_daily", "spotify_top_weekly",
436
+ "applemusic_top", "applemusic_daily", "applemusic_albums",
437
+ "itunes_top", "itunes_albums",
438
+ "shazam", "beatport",
439
+ "youtube", "youtube_tracks", "youtube_videos", "youtube_trends",
440
+ "amazon"
441
+ ]
442
+
443
+ Returns:
444
+ - dict: Chart entries containing album name, rank, and peak info.
445
+
446
+ Notes:
447
+ - Results are saved in working memory.
448
+ """
449
+
450
+ valid_chart_types = [
451
+ "spotify_viral_daily", "spotify_viral_weekly", "spotify_top_daily", "spotify_top_weekly",
452
+ "applemusic_top", "applemusic_daily", "applemusic_albums",
453
+ "itunes_top", "itunes_albums", "shazam", "beatport",
454
+ "youtube", "youtube_tracks", "youtube_videos", "youtube_trends", "amazon"
455
+ ]
456
+
457
+ if chart_type not in valid_chart_types:
458
+ raise ValueError(f"Invalid chart_type '{chart_type}'. Must be one of: {valid_chart_types}")
459
+
460
+ current_state = await ctx.store.get('state')
461
+ print(f"value of current_state on load inside of get_chart is: {current_state}")
462
+
463
+ #https://api.chartmetric.com/api/artist/:id/:type/charts
464
+
465
+ access_token = get_chartmetric_access_token_cached()
466
+
467
+
468
+ print("🚀 Called get_charts with artist_id:", artist_id)
469
+ print("🚀 Called get_charts with access_token:", access_token)
470
+
471
+ ##shoukd make dates of the chart dynamic later
472
+ ##need to give chart options in function description clearly
473
+
474
+ url = f"https://api.chartmetric.com/api/artist/{artist_id}/{chart_type}/charts?since=2025-03-01&until=2025-07-04"
475
+ headers = {
476
+ "Authorization": f"Bearer {access_token}"
477
+ }
478
+
479
+ response = requests.get(url, headers=headers)
480
+
481
+ if not response.ok:
482
+ print(f"❌ Request failed with status {response.status_code}: {response.text}")
483
+ return {}
484
+
485
+
486
+ data = response.json()
487
+ #print(f"data from get_charts is: {data}")
488
+ print("🚀 data call to get_charts successfully made!")
489
+
490
+ dataObj = data.get('obj',{})
491
+ #print(f"dataObj is {dataObj}")
492
+ dataObjEntries = dataObj.get('data',{})
493
+ dataObjEntries2 = dataObjEntries.get('entries',{})
494
+ #print(f"dataObjEntries2 is {dataObjEntries2}")
495
+
496
+ relevant_details = []
497
+ for entry in dataObjEntries2:
498
+ print(f"entry is: {entry}")
499
+ stuffToSave = { "album": entry["name"], "pre-rank": entry["pre_rank"], "peak": entry["peak_rank"], "peak_day": entry["peak_date"], "rank": entry["rank"] }
500
+ print(f"stuff to save is: {stuffToSave}")
501
+ relevant_details.append(stuffToSave)
502
+
503
+ print(f"value of relevant_dtails is: {relevant_details}")
504
+
505
+ if "working_notes" not in current_state:
506
+ current_state["working_notes"] = {}
507
+
508
+ if f"charts_data for {artist_id}" not in current_state["working_notes"]:
509
+ current_state["working_notes"][f"charts_data for {artist_id}"] = {}
510
+
511
+ current_state["working_notes"][f"charts_data for {artist_id}"][chart_type] = relevant_details
512
+ await ctx.store.set('state', current_state)
513
+
514
+ return {
515
+ "artist_id": artist_id,
516
+ "chart_data": relevant_details
517
+ }
518
+
519
 
520
 
521
 
 
532
  #find_artist_id_for_artist_tool = FunctionTool(fn=find_artist_id_for_artist)
533
  #get_instagram_audience_stats_tool = FunctionTool(fn=get_instagram_audience_stats)
534
  #get_similar_artists_tool = FunctionTool(fn=get_similar_artists)
535
+
536
  manager_agent = ReActAgent(
537
  name="ManagerAgent",
538
  description="Manager agent decides which other agents to use, and is decision maker",
539
+ system_prompt=(
540
+ "You are the manager agent. You do not collect data yourself. You delegate tasks to other agents.\n\n"
541
+ "Your responsibilities are:\n"
542
+ "- Receive the user’s question\n"
543
+ "- Decide whether StreamingChartAgent or SocialMediaDataAgent or SimilarityAgent (or two or all) should handle the request\n"
544
+ "+ If the question is about social media audience data (TikTok, Instagram, YouTube), use SocialMediaDataAgent."
545
+ "+ If the question is about chart positions, chart history, or streaming rankings, use StreamingChartAgent."
546
+ "- Wait for their responses and evaluate whether the question has been sufficiently answered\n"
547
+ ),
548
+ llm=llm,
549
+ can_handoff_to=["SocialMediaDataAgent", "SimilarityAgent", "StreamingChartAgent"]
550
  )
551
 
552
+ streaming_chart_agent = ReActAgent(
553
+ name="StreamingChartAgent",
554
+ description="agent to retrieve streaming chart data for the artist being researched",
555
+ system_prompt=("You are a research agent that retrieves streaming chart information about an artist"),
556
+ llm=llm,
557
+ tools=[get_charts, find_artist_id_for_artist],
558
+ can_handoff_to=["ManagerAgent", "SimilarityAgent", "SocialMediaDataAgent"]
559
+ )
560
 
561
 
562
  social_media_data_agent = ReActAgent(#try with Function Agents first, change to ReAct agents if needed/performance is poor.
 
572
  "- Your tools are only for Instagram and TikTok and Youtube data.\n"
573
  )
574
  ,
575
+ llm=llmHigher,
576
  tools=[get_instagram_audience_data, find_artist_id_for_artist, get_tiktok_audience_data, get_youtube_audience_data],
577
+ can_handoff_to=["ManagerAgent", "SimilarityAgent", "StreamingChartAgent"]#allow it to handoff to all other agents
578
+ )
579
+
580
+ streaming_chart_agent = ReActAgent(
581
+ name="StreamingChartAgent",
582
+ description="agent to retrieve streaming chart data for the artist being researched",
583
+ system_prompt=("You are a research agent that retrieves streaming chart information about an artist"),
584
+ llm=llm,
585
+ tools=[get_charts, find_artist_id_for_artist],
586
+ can_handoff_to=["ManagerAgent", "SimilarityAgent", "SocialMediaDataAgent"]
587
  )
588
 
589
  similarity_agent = ReActAgent(
 
594
  ),
595
  llm=llm,
596
  tools=[get_similar_artists, find_artist_id_for_artist],
597
+ can_handoff_to=["ManagerAgent", "SocialMediaDataAgent", "StreamingChartAgent"]
598
  )
599
 
600
 
 
603
 
604
 
605
 
606
+ async def main(chosen_artist, prompt):
607
  #response = await workflow.run(user_msg="What is Bertie Blackman's Chartmetric artist ID?"
608
  #, ctx=ctx) python llamaOaAgent.py
609
  #chosen_artist = "Kenan Doğulu"
 
617
  f"Who are the most similar artists to {chosen_artist}? Based on sonic qualities and existing audience data, who are his closest peers?",
618
  f"Who is listening to artists similar to {chosen_artist}? What does the audience profile of {chosen_artist}'s peer artists look like, and where does it overlap with his?",
619
  f"Where can {chosen_artist} find new listeners? Which specific playlists (on Spotify, Apple Music, etc.) are crucial for reaching the fans of these similar artists?",
620
+ f"Who should be {chosen_artist}'s audience? Based on all the available data, what does the ideal 'extended audience' look like that we should be targeting?",
621
+ f"what time of year do {chosen_artist}'s albums perform the best in the charts?"
622
  ]
623
  overall_answers = ""
624
  overall_answers2 = {}
625
 
626
+ all_states = {}
627
+
628
  for (index, user_msg) in enumerate(questions):
629
 
630
  print(f"starting questions {index + 1}")
 
633
 
634
  #create/re-create workflow with new question as user_msg
635
  workflow = AgentWorkflow(
636
+ agents=[similarity_agent, social_media_data_agent, manager_agent, streaming_chart_agent],
637
  root_agent=manager_agent.name,
638
  initial_state={"working_notes": {}, "user question": user_msg, "users language": "English"}
639
  )
 
641
  # run the workflow with context
642
  ctx = Context(workflow)
643
 
644
+
645
+
646
+
647
  handler = workflow.run(user_msg=user_msg, ctx=ctx)
648
  current_agent = None
649
  current_tool_calls = ""
 
701
  print(f"🔨 Calling Tool: {event.tool_name}")
702
  print(f" With arguments: {event.tool_kwargs}")
703
 
704
+ state = await ctx.store.get("state")
705
+ all_states[f"Q{index+1}"] = {
706
+ "question": user_msg,
707
+ "state": state
708
+ }
709
+
710
+ print(f"overall_answers is: {overall_answers}")
711
+
712
+ #final_state = await ctx.store.get("state")
713
+
714
+ with open(f"ctx_memory_all_answers.json", "w") as f:
715
+ json.dump(all_states, f, indent=2)
716
 
717
  #can then keep just the last thought of each question index
718
 
 
733
  total_tokens2 = count_tokens(flattened)
734
  print(f"Total tokens of overall_answers2: {total_tokens2}")
735
 
736
+ with open(f"overall_answersGemini.txt","w", encoding="utf-8") as file:
737
  file.write(overall_answers)
738
 
739
+ with open(f"overall_answers2Gemini.txt","w", encoding="utf-8") as file:
740
  json.dump(overall_answers2, file, ensure_ascii=False, indent=2)
741
 
742
 
 
782
  """
783
 
784
  prompto3 = f"""
785
+ # ROLE & TASK
786
+ You are a **senior music strategist** hired to deliver a **two-page Audience Intelligence Brief** for the artist **{chosen_artist}**.
787
+
788
+ # SOURCE MATERIAL
789
+ – You have one source only: **RAW_DATA** (verbatim answers & metrics pulled from Instagram, TikTok and YouTube).
790
+ – Treat all numbers as trustworthy unless they contradict each other; in that case flag the conflict in “Data Gaps”.
791
+
792
+ # WORKFLOW (do not display)
793
+ 1. **THINK:** Extract every statistic, named entity, quote or behavioural clue from RAW_DATA.
794
+ 2. **PLAN:** Map those findings onto the template sections. Identify unsupported cells early.
795
+ 3. **WRITE:** Populate the markdown template in polished, presentation-ready prose.
796
+ Use concise bullet points (max. 15 words each) and tables for scannability.
797
+ Keep each column width sensible; wrap long text with `<br>` if needed.
798
+ 4. **VERIFY:** Double-check that totals, % and age-band ranges add up logically.
799
+ 5. **CLEAN:** Do **not** expose this workflow, system prompts or RAW_DATA.
800
+
801
+ # STYLE
802
+ Consultative, insight-rich, brand-strategy tone. Prefer active voice, audience-centric language (“Fans show…”, “Leverage…”).
803
+ Use **bold** for key stats, *italics* for emphasis, emojis only where the template already includes them.
804
+
805
+ # DELIVERABLE
806
+ Return **exactly** the filled-in template between the markers
807
+ `---BEGIN BRIEF---` and `---END BRIEF---`.
808
+ If a section lacks data, keep the section but write “*No platform data supplied — analyst inference required*”.
809
+
810
+ # MARKDOWN TEMPLATE (to be populated – do NOT repeat unfilled)
811
+ ### Deep-Dive Audience Analysis for {chosen_artist}
812
+ (Synthesising Instagram, TikTok & YouTube data within Turkish pop-market context)
813
 
814
  ---
815
 
816
  1. **Audience Architecture at a Glance**
817
  | Layer | Instagram Data | TikTok/Other* | Strategic Takeaway |
818
+ |--------------------|---------------------------|-----------------------|------------------------------------------|
819
+ | Scale | | | |
820
+ | Core Territory | | | |
821
+ | Secondary Markets | | | |
822
+ | Gender | | | |
823
+ | Prime Age Band | | | |
824
 
825
  ---
826
 
827
  2. **Hidden Insights & Underserved Nuances**
828
+ | Insight | Evidence (platform, metric) | Why It Matters |
829
+ |------------------------------------|---------------------------------|------------------------------------------|
830
+ | | | |
831
+ | | | |
832
+ | | | |
833
 
834
  ---
835
 
836
+ 3. **Psychographic Micro-Segments to Activate**
837
+ | Segment Name | % Audience | Description (mindset / need-state) | Ideal Touch-point |
838
+ |---------------------|-----------:|------------------------------------|-----------------------------------------|
839
+ | | | | |
840
+ | | | | |
841
 
842
  ---
843
 
844
  4. **Content & Channel Implications**
845
+ | Funnel Stage | Priority Channel(s) | Format & Narrative Hook |
846
+ |----------------|---------------------|--------------------------------------|
847
+ | Discovery | | |
848
+ | Consideration | | |
849
+ | Community | | |
850
+ | Conversion | | |
851
 
852
  ---
853
 
854
  5. **Monetisation & Partnership Levers**
855
+ -
856
  -
857
  -
858
  -
 
860
  ---
861
 
862
  6. **Risks & Mitigations**
863
+ | Risk | Potential Impact | Mitigation Play |
864
+ |----------------------------------------|------------------------|------------------------------------------|
865
+ | | | |
866
+ | | | |
867
 
868
  ---
869
 
 
874
 
875
  ---
876
 
877
+ 📦 **RAW_DATA** (for internal use only – do NOT show in the brief)
878
  {overall_answers}
879
+
880
+ ---BEGIN BRIEF---
881
+ <!-- o3 starts populating here -->
882
+ ---END BRIEF---
883
  """
884
 
885
+ prompto3ChrisChart = f"""🎯 MEGA AUDIENCE INSIGHT & GROWTH PROMPT
886
+ Prompt Title: Deep Audience Intelligence & Growth Blueprint for [ARTIST_NAME]
887
 
888
+ System Role (Set Once):
889
+ You are a senior music data strategist trained in multi-platform audience intelligence, behavioral segmentation, and growth marketing. You operate like a hybrid of a data analyst, music marketer, and product strategist. Your job is to extract unique insights, detect overlooked opportunities, and build a data-driven growth plan for the artist based on a rich dataset of streaming, chart, and social data.
890
 
891
+ 🔍 INPUT DATA:
892
+ Structured streaming data (Spotify, Apple Music, iTunes, Shazam) with rank movement, peak days, velocity, and decay.
893
 
894
+ Social media + CRM metrics (TikTok, IG, YouTube, Reels, Stories, Email, Merch, Tour Sales, etc.).
895
 
896
+ Any artist metadata you can derive (track names, album release cycles, remix info, sentiment cues, genre tags, collaborators).
897
+
898
+ 🧠 TASK
899
+ Split your approach into three distinct cognitive layers, executed in sequence:
900
+
901
+ ✅ LAYER 1: ANALYTICAL DEEP DIVE
902
+ Understand the data in its rawest form.
903
+
904
+ Detect patterns in streaming velocity, seasonal performance, and Shazam conversion.
905
+
906
+ Surface anomalies — outlier peaks, remix vs original inconsistencies, platform skews.
907
+
908
+ Build segmentations across:
909
+
910
+ Demographics (inferred via geo and platform)
911
+
912
+ Behavioral (engagement, replay rate, completion, skip/save behavior)
913
+
914
+ Content type affinity (e.g., club mix vs acoustic vs emotional lyrics)
915
+
916
+ Identify:
917
+
918
+ Top 3–5 most influential formats (content, platform, track type)
919
+
920
+ 2–3 examples of platform crossover lags (e.g., Shazam peak → Spotify delay)
921
+
922
+ Fanbase decay curves (where and when attention drops off)
923
+
924
+ 🧠 LAYER 2: STRATEGIC REASONING
925
+ Generate hypotheses and opportunity clusters.
926
+
927
+ Audience Gaps:
928
+
929
+ Where is the artist underperforming?
930
+
931
+ What similar audiences (adjacent genres, demos, cities) are reachable?
932
+
933
+ Cluster Fans into Personas based on behavior + geo:
934
+ Example labels:
935
+
936
+ “Shazam-driven club-goers in Southern Europe”
937
+
938
+ “Loyal iTunes buyers over 40 in Central Asia”
939
+
940
+ “Spotify Weekly repeaters with remix preference in Berlin”
941
+
942
+ For each persona cluster, answer:
943
+
944
+ What drives their behavior?
945
+
946
+ Where can we find more like them?
947
+
948
+ Which platform(s) matter most?
949
+
950
+ Propose 3–4 testable hypotheses about:
951
+
952
+ Timing strategies
953
+
954
+ Collaboration types
955
+
956
+ Format performance
957
 
958
+ Messaging tones (e.g. romantic, nostalgic, rebellious)
959
+
960
+ 🚀 LAYER 3: GROWTH & CAMPAIGN STRATEGY
961
+ Turn intelligence into a tactical plan.
962
+
963
+ Recommend:
964
+
965
+ 3 platform strategies, tailored to audience types (e.g. TikTok + Reels = Hook virality vs Apple = intimacy/purchase)
966
+
967
+ 3 content types likely to resonate with segments (e.g. stripped vocals for Gen Z on IG vs remix packs for DJs)
968
+
969
+ 2 partnership ideas — either influencer-led, playlist curators, or collab artists with overlapping fanbases
970
+
971
+ Suggest distribution timing:
972
+
973
+ What day, week, and month clusters have historically driven best results?
974
+
975
+ Layer this with social engagement cycles.
976
+
977
+ Design 1 bold, data-informed “Big Bet” campaign:
978
+
979
+ Could be a geo-targeted drop, genre mashup collab, remix competition, or a multi-platform narrative series.
980
+
981
+ 🧪 OUTPUT FORMAT:
982
+ markdown
983
+ Copy
984
+ Edit
985
+ # Artist Audience Intelligence & Growth Blueprint: [Artist Name]
986
+
987
+ ## 1. Overview
988
+ Short summary of overall patterns, growth arcs, and platform behaviors.
989
+
990
+ ## 2. Key Segments
991
+ - Persona 1: “...” → Description, platforms, geo, behavior
992
+ - Persona 2: ...
993
+ - Persona 3: ...
994
+
995
+ ## 3. Strategic Observations
996
+ - Opportunity gaps
997
+ - Surprising over/under performance
998
+ - Hypotheses
999
+
1000
+ ## 4. Marketing Recommendations
1001
+ ### A. Platform Strategy
1002
+ [List of 3, each with logic and examples]
1003
+
1004
+ ### B. Content Types to Emphasize
1005
+ [List of 3, with reasoning per segment]
1006
+
1007
+ ### C. Influencer/Partnership Strategy
1008
+ [2 ideas with audience alignment logic]
1009
+
1010
+ ## 5. Big Bet Growth Campaign
1011
+ Title + concept + rationale
1012
+
1013
+ Your data is {overall_answers}
1014
+ """
1015
+
1016
+
1017
+
1018
+
1019
+
1020
+ final_prompt = prompt + f"This report is about artist {chosen_artist}" + f"your sole data source is: {overall_answers}"
1021
+ ##that should ensure that whatever prompt is entered, the correct artist and data source is still passed down.
1022
 
1023
 
1024
  #count tokens anyway, for later usage:
 
1027
 
1028
  max_tokens = 16384 - total_tokens - 200
1029
 
 
 
1030
  messages=[
1031
  {
1032
  "role": "system",
 
1034
  },
1035
  {
1036
  "role": "user",
1037
+ "content": final_prompt
1038
+ }
1039
+ ]
1040
+
1041
+ response = client.responses.create(
1042
+ model="o3",
1043
+ input=[
1044
+ {
1045
+ "role": "developer",
1046
+ "content": [
1047
+ {
1048
+ "type": "input_text",
1049
+ "text": (
1050
+ "You are a precise music industry data analyst. "
1051
+ "Be structured, factual, and preserve all stats given."
1052
+ )
1053
+ }
1054
+ ]
1055
+ },
1056
+ {
1057
+ "role": "user",
1058
+ "content": [
1059
+ {
1060
+ "type": "input_text",
1061
+ "text": prompto3
1062
+ }
1063
+ ]
1064
  }
1065
  ],
1066
+ text={
1067
+ "format": {
1068
+ "type": "text"
1069
+ }
1070
+ },
1071
+ reasoning={
1072
+ "effort": "medium",
1073
+ "summary": "auto"
1074
+ },
1075
+ tools=[],
1076
+ store=True
1077
+ )
1078
+
1079
 
1080
+ print(response.output_text)
1081
+ two_pager_document = response.output_text
1082
 
1083
  #add generation of two_pager_part2
1084
 
 
1086
  #for question in formal_questions:
1087
  print(f"overall_answer2 is {overall_answers2}")
1088
 
1089
+ with open(f"{chosen_artist}.txt","w", encoding="utf-8") as file:
1090
  file.write(two_pager_document)
1091
 
1092
  return two_pager_document
 
1094
 
1095
  demo = gr.Interface(
1096
  fn=main,
1097
+ inputs=["text", "text"], #one for artist_name, other for prompt
1098
  outputs="text",
1099
  title="artist report generator",
1100
  description="generate report for artist"
appOld.py ADDED
@@ -0,0 +1,789 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #try using existing logic, but add ctx/memory that llamindex allows
2
+
3
+ #do autonomous llamagents
4
+
5
+ from llama_index.core.tools import FunctionTool
6
+ from llama_index.llms.openai import OpenAI
7
+ from dotenv import load_dotenv
8
+ #from llama_index.llms.huggingface_api import HuggingFaceInferenceAPI
9
+ from llama_index.core.agent.workflow import AgentWorkflow, FunctionAgent, ReActAgent #can also import ReActAgent or FunctionAgent from this
10
+ from llama_index.core.tools import FunctionTool
11
+ from llama_index.core.workflow import Context
12
+ import os
13
+ from functools import lru_cache
14
+ import asyncio
15
+ import requests
16
+ from llama_index.core.agent.workflow import (
17
+ AgentInput,
18
+ AgentOutput,
19
+ ToolCall,
20
+ ToolCallResult,
21
+ AgentStream,
22
+ )
23
+ import openai
24
+ import tiktoken
25
+ import requests
26
+ import json
27
+ import gradio as gr
28
+
29
+
30
+
31
+ #from llama_index.llms.google_gemini import GoogleGenAI
32
+ #from google.genai import types
33
+
34
+ load_dotenv()
35
+
36
+ #llm = OpenAI(model="gpt-4o-mini")
37
+
38
+ llm = OpenAI(
39
+ model="gpt-4o-mini", # or "gpt-3.5-turbo"
40
+ api_key=os.getenv('OPENAI_API_KEY'), # You can also set this via the OPENAI_API_KEY environment variable
41
+ streaming=True
42
+ )
43
+
44
+ openai.api_key = os.getenv("OPENAI_API_KEY")
45
+ #use gemini
46
+
47
+ #set api_key in .env for gemini
48
+ #llmGemini = GoogleGenAI(model="gemini-2.5-pro")
49
+
50
+ #can use search as AI
51
+ #google_search_tool = types.Tool(
52
+ #google_search=types.GoogleSearch()
53
+ #)#should be able to pass as tool?
54
+
55
+
56
+
57
+ @lru_cache(maxsize=1)
58
+ def get_chartmetric_access_token_cached() -> str | None:
59
+ print("🔑 Fetching new Chartmetric token")
60
+ return get_chartmetric_access_token_with_refresh()
61
+
62
+ #@function_tool
63
+ def get_chartmetric_access_token_with_refresh() -> str or None:
64
+ """
65
+ Retrieves an access token from Chartmetric. You need to use this before you can use any other function involving chartmetric
66
+
67
+ """
68
+ #current_state = await ctx.get('state')
69
+
70
+
71
+ refresh_token = 'izPNc1uMM7A13dvWGs0Gij3rfMTKV0K24ADFfcHviaOPWxc35ZsNuYqlQNb5BVyG'
72
+
73
+ endpoint = 'https://api.chartmetric.com/api/token'
74
+ headers = {
75
+ 'Content-Type': 'application/json'
76
+ }
77
+ payload = {
78
+ 'refreshtoken': refresh_token
79
+ }
80
+
81
+ try:
82
+ response = requests.post(endpoint, headers=headers, json=payload)
83
+ if not response.ok:
84
+ raise Exception(f"Token request failed: {response.status_code} {response.reason}")
85
+
86
+ data = response.json()
87
+ print("Access token retrieved:", data.get('token'),{})
88
+
89
+ #if "working_notes" not in current_state:
90
+ #current_state["working_notes"] = {}
91
+
92
+ access_token = data.get('token')# This is your bearer token for future API calls
93
+ #current_state["working_notes"]["access_token"] = access_token
94
+
95
+ #await ctx.set("state", current_state)
96
+ return access_token
97
+
98
+ except Exception as e:
99
+ print("Error retrieving Chartmetric access token:", str(e))
100
+ return None
101
+
102
+
103
+
104
+ #@function_tool
105
+ async def find_artist_id_for_artist(ctx: Context, artist_name: str) -> int:
106
+ """
107
+ Retrieves artist_id for the artist you want to search on the chartmetric system .
108
+
109
+
110
+ """
111
+ current_state = await ctx.get('state')
112
+
113
+ access_token = get_chartmetric_access_token_cached()
114
+
115
+ url = f'https://api.chartmetric.com/api/search?q={artist_name}&type=artists'
116
+
117
+ headers = {
118
+ "Authorization": f"Bearer {access_token}"
119
+ }
120
+
121
+ try:
122
+ response = requests.get(url, headers=headers)
123
+
124
+ if not response.ok:
125
+ raise Exception(f"artist_id request failed: {response.status_code} {response.reason}")
126
+
127
+ data = response.json()
128
+ print("Raw response data:", data)
129
+
130
+ # Safely access first matched artist
131
+ artists = data.get("obj", {}).get("artists", [])
132
+
133
+ if not artists:
134
+ print(f"No artists found matching '{artist_name}'.")
135
+ return None
136
+
137
+ artist_id = artists[0].get('id',{})
138
+
139
+ # Update state and persist it
140
+ if "working_notes" not in current_state:
141
+ current_state["working_notes"] = {}
142
+
143
+ current_state["working_notes"][artist_name] = artist_id
144
+ await ctx.set("state", current_state) # 🟢 Save the updated state
145
+
146
+ return artist_id
147
+
148
+ except Exception as e:
149
+ print("Error retrieving Chartmetric artist_id:", str(e))
150
+ return None
151
+
152
+ #@function_tool
153
+ async def get_similar_artists(ctx: Context, artist_id: int) -> dict:
154
+ """
155
+ Retrieve a list of similar artists from Chartmetric based on a given artist ID.
156
+
157
+ Parameters:
158
+ - artist_id (int): The Chartmetric artist ID.
159
+
160
+ Returns:
161
+ - dict: A dictionary of similar artists (up to 5).
162
+
163
+ Notes:
164
+ - Results are stored in working memory under "similar_artists".
165
+ """
166
+ current_state = await ctx.get('state')
167
+
168
+ access_token = get_chartmetric_access_token_cached() # Assuming this is defined elsewhere
169
+ print("access_token for get_similar_artists api call obatined!")
170
+
171
+ url = f"https://api.chartmetric.com/api/artist/{artist_id}/relatedartists?limit=3"
172
+ headers = {
173
+ "Authorization": f"Bearer {access_token}"
174
+ }
175
+
176
+ try:
177
+ response = requests.get(url, headers=headers)
178
+ if not response.ok:
179
+ raise Exception(f"Related artists request failed: {response.status_code} {response.reason}")
180
+
181
+ data = response.json()
182
+ print("data returned from get_similar_artists is:", data)
183
+
184
+
185
+ similar_artists = data.get('obj', {})
186
+
187
+ if "working_notes" not in current_state:
188
+ current_state["working_notes"] = {}
189
+
190
+ current_state["working_notes"]["similar_artists"] = similar_artists
191
+ await ctx.set('state', current_state)
192
+
193
+ return similar_artists
194
+
195
+ except Exception as e:
196
+ print("Error retrieving similar artists:", str(e))
197
+ return None
198
+
199
+
200
+ async def get_youtube_audience_data(ctx: Context, artist_id: str) -> dict:
201
+ """
202
+ Retrieve Youtube audience data for a given artist, using Chartmetric API.
203
+
204
+ Parameters:
205
+ - artist_id (int): The Chartmetric artist ID.
206
+
207
+ Returns:
208
+ - dict: A dictionary of similar artists (up to 5).
209
+
210
+ Notes:
211
+ - Results are saved in working memory.
212
+ """
213
+ current_state = await ctx.get('state')
214
+
215
+ access_token = get_chartmetric_access_token_cached()
216
+
217
+
218
+ print("🚀 Called get_Youtube with artist_id:", artist_id)
219
+ print("🚀 Called get_Youtube with access_token:", access_token)
220
+
221
+
222
+ url = f"https://api.chartmetric.com/api/artist/{artist_id}/youtube-audience-stats"
223
+ headers = {
224
+ "Authorization": f"Bearer {access_token}"
225
+ }
226
+
227
+ response = requests.get(url, headers=headers)
228
+
229
+ if not response.ok:
230
+ if response.status_code == 404:
231
+ print(f"⚠️ No YouTube data found for artist {artist_id}")
232
+ return {}
233
+
234
+
235
+ data = response.json()
236
+ print(f"data from get_Youtube is: {data}")
237
+
238
+ dataObj = data.get('obj',{})
239
+
240
+ print("Info from get_tiktok_audience_data is:", dataObj)
241
+
242
+ compressed_notable_followers = []
243
+ for follower in dataObj["notable_subscribers"]:
244
+ #pprint(f"follower in dataObj is: {follower}")
245
+
246
+ new_data = {}
247
+
248
+ new_data["custom_name"] = follower.get("custom_name", {})
249
+ new_data["subscribers"] = follower["subscribers"]
250
+ new_data["engagements"] = follower["engagements"]
251
+
252
+ compressed_notable_followers.append(new_data)
253
+
254
+
255
+ dict_to_return = {"top_countries": dataObj["top_countries"], "audience_gender_by_age": dataObj["audience_genders_per_age"], "audience_genders": dataObj["audience_genders"], "top_followers": compressed_notable_followers,
256
+ "subscribers": dataObj["subscribers"], "avg_likes_per_post": dataObj["avg_likes_per_post"], "avg_commments_per_post": dataObj["avg_commments_per_post"],
257
+ "engagement_rate": dataObj["engagement_rate"]
258
+
259
+ }
260
+
261
+ if "working_notes" not in current_state:
262
+ current_state["working_notes"] = {}
263
+
264
+ youtube_audience_stats = dict_to_return
265
+ print(f"youtube_audience_stats are: {youtube_audience_stats}")
266
+ current_state["working_notes"][f"youtube_audience_data for artist {artist_id}"] = youtube_audience_stats
267
+ await ctx.set('state', current_state)
268
+
269
+ return { f"youtube_audience_data for artist {artist_id}": youtube_audience_stats}
270
+
271
+
272
+
273
+
274
+
275
+
276
+
277
+ async def get_tiktok_audience_data(ctx: Context, artist_id: str) -> dict:
278
+ """
279
+ Retrieve TikTok audience data for a given artist using Chartmetric API.
280
+
281
+ Parameters:
282
+ - artist_id (str): The Chartmetric artist ID.
283
+
284
+ Returns:
285
+ - dict: Instagram audience breakdown.
286
+
287
+ Notes:
288
+ - Results are saved in working memory.
289
+ """
290
+ current_state = await ctx.get('state')
291
+
292
+ access_token = get_chartmetric_access_token_cached()
293
+
294
+
295
+ print("🚀 Called get_tiktok_audience_data with artist_id:", artist_id)
296
+ print("🚀 Called get_tiktok_audience_data with access_token:", access_token)
297
+
298
+ url = f"https://api.chartmetric.com/api/artist/{artist_id}/tiktok-audience-stats"
299
+ headers = {
300
+ "Authorization": f"Bearer {access_token}"
301
+ }
302
+
303
+ response = requests.get(url, headers=headers)
304
+
305
+ if not response.ok:
306
+ raise Exception(f"API request failed: {response.status_code} {response.reason}")
307
+
308
+ data = response.json()
309
+ #print(f"data from get_tiktok_audience_data is: {data}")
310
+
311
+ dataObj = data.get('obj',{})
312
+
313
+ #print("Info from get_tiktok_audience_data is:", dataObj)
314
+
315
+ compressed_notable_followers = []
316
+ for follower in dataObj.get("notable_followers", []):
317
+ #print(f"follower in dataObj is: {follower}")
318
+
319
+ new_data = {}
320
+ new_data["username"] = follower["username"]
321
+ new_data["followers"] = follower["followers"]
322
+ new_data["engagement"] = follower["engagements"]
323
+
324
+ compressed_notable_followers.append(new_data)
325
+
326
+
327
+ dict_to_return = {"top_countries": dataObj["top_countries"], "audience_gender_by_age": dataObj["audience_genders_per_age"], "audience_genders": dataObj["audience_genders"], "top_followers": compressed_notable_followers,
328
+ "followers": dataObj["followers"], "avg_likes_per_post": dataObj["avg_likes_per_post"], "avg_commments_per_post": dataObj["avg_commments_per_post"],
329
+ "engagement_rate": dataObj["engagement_rate"]
330
+
331
+ }
332
+ if "working_notes" not in current_state:
333
+ current_state["working_notes"] = {}
334
+
335
+ tiktok_audience_stats = dict_to_return
336
+ print(f"tiktok_audience_data are: {tiktok_audience_stats}")
337
+ current_state["working_notes"][f"tiktok_audience_data for artist {artist_id}"] = tiktok_audience_stats
338
+ await ctx.set('state', current_state)
339
+
340
+ return { f"tiktok_audience_data for artist {artist_id}": tiktok_audience_stats}
341
+
342
+ #choose which parts to return
343
+
344
+
345
+
346
+
347
+
348
+
349
+ #@function_tool
350
+ async def get_instagram_audience_data(ctx: Context, artist_id: str) -> dict:
351
+ """
352
+ Retrieve Instagram audience statistics for a given artist using Chartmetric.
353
+
354
+ Parameters:
355
+ - artist_id (str): The Chartmetric artist ID.
356
+
357
+ Returns:
358
+ - dict: Instagram audience breakdown.
359
+
360
+ Notes:
361
+ - Results are saved in working memory.
362
+ """
363
+ #perhaps just have it get access_token inside here
364
+ #access_token = get_chartmetric_access_token_with_refresh()
365
+
366
+ current_state = await ctx.get('state')
367
+
368
+ access_token = get_chartmetric_access_token_cached()
369
+
370
+
371
+ print("🚀 Called get_instagram_audience_stats with artist_id:", artist_id)
372
+ print("🚀 Called get_instagram_audience_stats with access_token:", access_token)
373
+
374
+ url = f"https://api.chartmetric.com/api/artist/{artist_id}/instagram-audience-stats"
375
+ headers = {
376
+ "Authorization": f"Bearer {access_token}"
377
+ }
378
+
379
+ response = requests.get(url, headers=headers)
380
+
381
+ if not response.ok:
382
+ raise Exception(f"API request failed: {response.status_code} {response.reason}")
383
+
384
+ data = response.json()
385
+ print(f"data from api call is: {data}")
386
+ print("Info from platform Instagram is:", data.get("obj"))
387
+
388
+
389
+ if "working_notes" not in current_state:
390
+ current_state["working_notes"] = {}
391
+
392
+ instagram_audience_stats = data.get('obj', {})
393
+ current_state["working_notes"][f"instagram_audience_data for artist {artist_id}"] = instagram_audience_stats
394
+ await ctx.set('state', current_state)
395
+
396
+ return { f"instagram_audience_data for artist {artist_id}": instagram_audience_stats}
397
+
398
+
399
+
400
+
401
+
402
+
403
+
404
+ #and that code which allows logging of every step of the memory/thought process
405
+
406
+ #keep teh cahce of chartmetric api, attached to function that gets api_key, which is inserted into each relevant api
407
+ #find_artist_id_for_artist_tool = FunctionTool.from_function(find_artist_id_for_artist)
408
+ #get_instagram_audience_stats_tool = FunctionTool.from_function(get_instagram_audience_stats)
409
+ #get_similar_artists = FunctionTool.from_function(get_similar_artists)
410
+
411
+
412
+ # Wrap your function
413
+ #find_artist_id_for_artist_tool = FunctionTool(fn=find_artist_id_for_artist)
414
+ #get_instagram_audience_stats_tool = FunctionTool(fn=get_instagram_audience_stats)
415
+ #get_similar_artists_tool = FunctionTool(fn=get_similar_artists)
416
+ manager_agent = ReActAgent(
417
+ name="ManagerAgent",
418
+ description="Manager agent decides which other agents to use, and is decision maker",
419
+ system_prompt=("You are the manager agent, you decide which other agents to use and hand-off to."
420
+ "You decide when the answer is ready to be returned to the user"
421
+ ),
422
+ can_handoff_to=["SocialMediaDataAgent", "SimilarityAgent"]
423
+ )
424
+
425
+
426
+
427
+ social_media_data_agent = ReActAgent(#try with Function Agents first, change to ReAct agents if needed/performance is poor.
428
+ name="SocialMediaDataAgent",
429
+ description="agent to source data about artists from social media data, using chartmetric api",
430
+ system_prompt=(
431
+ "You are a research agent that uses social media data to analyze artist audiences via Chartmetric.\n"
432
+ "- Always use **both** Instagram and TikTok and Youtube data as your default behavior when analyzing artists.\n"
433
+ "- Do NOT choose one over the other unless explicitly told to focus on one.\n"
434
+ "- Always call 'get_instagram_audience_stats' AND 'get_tiktok_audience_data' AND 'get_youtube_audience_data' when gathering audience data.\n"
435
+ "- Do NOT assume artist names. Only use 'find_artist_id_for_artist' with real artist names provided by the user.\n"
436
+ "- If the user needs information about similar artists, HAND OFF to the SimilarityAgent — do NOT attempt it yourself.\n"
437
+ "- Your tools are only for Instagram and TikTok and Youtube data.\n"
438
+ )
439
+ ,
440
+ llm=llm,
441
+ tools=[get_instagram_audience_data, find_artist_id_for_artist, get_tiktok_audience_data, get_youtube_audience_data],
442
+ can_handoff_to=["ManagerAgent", "SimilarityAgent"]#allow it to handoff to all other agents
443
+ )
444
+
445
+ similarity_agent = ReActAgent(
446
+ name="SimilarityAgent",
447
+ description="agent to find similar artists to the artist being research, using chartmetric api",
448
+ system_prompt=("You are a research agent that looks for similar artists to the artist you are researching, in order to understand how the artist can copy the growth of similar artists who are larger."
449
+ "you can handoff to SocialMediaDataAgent, in order to find information about the followers of similar artists"
450
+ ),
451
+ llm=llm,
452
+ tools=[get_similar_artists, find_artist_id_for_artist],
453
+ can_handoff_to=["ManagerAgent", "SocialMediaDataAgent"]
454
+ )
455
+
456
+
457
+
458
+
459
+
460
+
461
+
462
+ async def main(chosen_artist):
463
+ #response = await workflow.run(user_msg="What is Bertie Blackman's Chartmetric artist ID?"
464
+ #, ctx=ctx) python llamaOaAgent.py
465
+ #chosen_artist = "Kenan Doğulu"
466
+
467
+ questions = [
468
+ f"Who are {chosen_artist}'s fans and what are their core demographics?",
469
+ f"Where are {chosen_artist}'s fans located: which countries and cities have the highest concentration of my listeners?",
470
+ f"What are the broader interests of {chosen_artist}'s fans beyond his music? What TV shows, films, or books do they consume? What are their other lifestyle affinities (e.g., sports, fashion, art, hobbies)?",
471
+ f"Where do {chosen_artist}'s fans gather online? What specific online communities, like subreddits or forums, are they active in?",
472
+ f"How can {chosen_artist} best reach his fans? Based on their online behaviour and interests, what are the most effective channels to engage them?",
473
+ f"Who are the most similar artists to {chosen_artist}? Based on sonic qualities and existing audience data, who are his closest peers?",
474
+ f"Who is listening to artists similar to {chosen_artist}? What does the audience profile of {chosen_artist}'s peer artists look like, and where does it overlap with his?",
475
+ f"Where can {chosen_artist} find new listeners? Which specific playlists (on Spotify, Apple Music, etc.) are crucial for reaching the fans of these similar artists?",
476
+ f"Who should be {chosen_artist}'s audience? Based on all the available data, what does the ideal 'extended audience' look like that we should be targeting?"
477
+ ]
478
+ overall_answers = ""
479
+ overall_answers2 = {}
480
+
481
+ for (index, user_msg) in enumerate(questions):
482
+
483
+ print(f"starting questions {index + 1}")
484
+
485
+ overall_answers2[index] = {"Thoughts": "", "Answer": ""}
486
+
487
+ #create/re-create workflow with new question as user_msg
488
+ workflow = AgentWorkflow(
489
+ agents=[similarity_agent, social_media_data_agent, manager_agent],
490
+ root_agent=manager_agent.name,
491
+ initial_state={"working_notes": {}, "user question": user_msg, "users language": "English"}
492
+ )
493
+
494
+ # run the workflow with context
495
+ ctx = Context(workflow)
496
+
497
+ handler = workflow.run(user_msg=user_msg, ctx=ctx)
498
+ current_agent = None
499
+ current_tool_calls = ""
500
+
501
+
502
+
503
+ async for event in handler.stream_events():
504
+ if (
505
+ hasattr(event, "current_agent_name")
506
+ and event.current_agent_name != current_agent
507
+ ):
508
+ current_agent = event.current_agent_name
509
+ print(f"\n{'='*50}")
510
+ print(f"🤖 Agent: {current_agent}")
511
+ print(f"{'='*50}\n")
512
+
513
+ elif isinstance(event, AgentOutput):
514
+ content = event.response.content.strip()
515
+ print("📤 Output:", content)
516
+
517
+ # New logic: extract Thought and Answer from any position
518
+ clean_answer_combined = ""
519
+ thought, answer = None, None
520
+
521
+ if "Thought:" in content:
522
+ if "Answer:" in content:
523
+ thought = content.split("Thought:")[1].split("Answer:")[0].strip()
524
+
525
+ else:
526
+ thought = content.split("Thought:")[1].strip()
527
+ overall_answers2[index]["Thoughts"] += "\n" + thought
528
+ clean_answer_combined += f"🧠 Thought: {thought}\n"
529
+
530
+ if "Answer:" in content:
531
+ answer = content.split("Answer:")[-1].strip()
532
+ overall_answers2[index]["Answer"] = answer
533
+ clean_answer_combined += f"✅ Answer: {answer}\n"
534
+
535
+ if clean_answer_combined:
536
+ question_header = f"\n### Q{index + 1}: {user_msg}\n"
537
+ overall_answers += question_header + clean_answer_combined + "\n"
538
+
539
+ # If either Thought or Answer was captured, append to overall_answers
540
+
541
+ if event.tool_calls:
542
+ print(
543
+ "🛠️ Planning to use tools:",
544
+ [call.tool_name for call in event.tool_calls],
545
+ )
546
+ elif isinstance(event, ToolCallResult):
547
+ print(f"🔧 Tool Result ({event.tool_name}):")
548
+ print(f" Arguments: {event.tool_kwargs}")
549
+ print(f" Output: {event.tool_output}")
550
+ elif isinstance(event, ToolCall):
551
+ print(f"🔨 Calling Tool: {event.tool_name}")
552
+ print(f" With arguments: {event.tool_kwargs}")
553
+
554
+ print(f"overall_answers is: {overall_answers}")
555
+
556
+ #can then keep just the last thought of each question index
557
+
558
+ def count_tokens(text, model="gpt-4o"):
559
+ encoding = tiktoken.encoding_for_model(model)
560
+ return len(encoding.encode(text))
561
+
562
+ total_tokens = count_tokens(overall_answers)
563
+ print(f"Total tokens of overall_answers: {total_tokens}")
564
+
565
+
566
+ # Build a single string
567
+ flattened = "\n\n".join(
568
+ f"Q{idx + 1}: {qa['Thoughts']}\n{qa['Answer']}"
569
+ for idx, qa in overall_answers2.items()
570
+ )
571
+
572
+ total_tokens2 = count_tokens(flattened)
573
+ print(f"Total tokens of overall_answers2: {total_tokens2}")
574
+
575
+ with open(f"overall_answers.txt","w", encoding="utf-8") as file:
576
+ file.write(overall_answers)
577
+
578
+ with open(f"overall_answers2.txt","w", encoding="utf-8") as file:
579
+ json.dump(overall_answers2, file, ensure_ascii=False, indent=2)
580
+
581
+
582
+
583
+ #now send overall_answers to LLM
584
+ prompt2 = f"""You need to assemble a document on the artist {chosen_artist} in the following structure
585
+
586
+ *SECTION TITLE* - Who is {chosen_artist}'s audience?
587
+
588
+ *SUB-SECTION HEADER* - 1. Core Profile
589
+ *answer using Q1 content*
590
+
591
+ *SUB-SECTION HEADER* - 2. What They Love (Beyond the Music)
592
+ *answer using Q2 content*
593
+
594
+ *SUB-SECTION HEADER* - 3. Audience Segments Identified
595
+ *answer using Q3 content*
596
+
597
+ *SUB-SECTION HEADER* - 4. Where They Hang Out Online
598
+ *answer using Q4 content*
599
+
600
+ You should assemble this document using the following data: {overall_answers}
601
+
602
+ """
603
+
604
+ prompt = f"""
605
+ You are assembling a detailed audience profile for artist {chosen_artist}.
606
+
607
+ IMPORTANT:
608
+ - You MUST preserve all statistics from the raw data.
609
+ - Do NOT paraphrase away key numbers like city %s, gender splits, or country breakdowns.
610
+ - Present these in structured markdown.
611
+
612
+ SECTION: Who is {chosen_artist}'s audience?
613
+
614
+ 1. Core Profile (use Q1)
615
+ 2. What They Love (use Q2)
616
+ 3. Audience Segments Identified (use Q3)
617
+ 4. Where They Hang Out Online (use Q4)
618
+
619
+ RAW DATA:
620
+ {overall_answers}
621
+ """
622
+
623
+ prompto3 = f"""
624
+ You are a senior music strategist preparing a 2-page audience intelligence brief for artist **{chosen_artist}**.
625
+
626
+ You have been given raw data from Instagram, TikTok, Chartmetric, and related tools.
627
+
628
+ ---
629
+
630
+ 🎯 INSTRUCTIONS:
631
+
632
+ - Use the detailed markdown template below as a **shell to be fully populated**.
633
+ - **Do not repeat the template unfilled.**
634
+ - Extract ALL relevant statistics, insights, and named entities from the raw data and synthesize them into this format.
635
+ - If a section is partially unsupported (e.g., missing TikTok data), write that clearly — but **still include the section fully**.
636
+ - Match the **tone, voice, and layout** of a presentation-ready strategy deck: consultative, insight-rich, structured.
637
+ - Use bullet points, tables, and formatting to ensure visual clarity.
638
+
639
+ ---
640
+
641
+ ### Deep‑Dive Audience Analysis for {chosen_artist}
642
+ (Synthesising Instagram data + Turkish pop‑market context)
643
+
644
+ ---
645
+
646
+ 1. **Audience Architecture at a Glance**
647
+ | Layer | Instagram Data | TikTok/Other* | Strategic Takeaway |
648
+ |--------------------|---------------------------|------------------------|-------------------------------------------|
649
+ | Scale | | | |
650
+ | Core Territory | | | |
651
+ | Secondary Markets | | | |
652
+ | Gender | | | |
653
+ | Prime Age Band | | | |
654
+
655
+ ---
656
+
657
+ 2. **Hidden Insights & Underserved Nuances**
658
+ | Insight | Evidence | Why It Matters |
659
+ |-----------------------------|-------------------------------------|------------------------------------------|
660
+ | | | |
661
+ | | | |
662
+ | | | |
663
+
664
+ ---
665
+
666
+ 3. **Psychographic Micro‑Segments to Activate**
667
+ | Segment Name | % Audience | Description | Ideal Touch���point |
668
+ |----------------------------|------------|---------------------------------------|------------------------------------------|
669
+ | | | | |
670
+ | | | | |
671
+
672
+ ---
673
+
674
+ 4. **Content & Channel Implications**
675
+ | Funnel Stage | Channel | Format Strategy |
676
+ |----------------|--------------------|-----------------------------------------|
677
+ | Discovery | | |
678
+ | Consideration | | |
679
+ | Community | | |
680
+ | Conversion | | |
681
+
682
+ ---
683
+
684
+ 5. **Monetisation & Partnership Levers**
685
+ - (Example: Stadium performance + merch collab with Galatasaray)
686
+ -
687
+ -
688
+ -
689
+
690
+ ---
691
+
692
+ 6. **Risks & Mitigations**
693
+ | Risk | Impact | Mitigation |
694
+ |-------------------------------|---------------------|-------------------------------------------|
695
+ | | | |
696
+ | | | |
697
+
698
+ ---
699
+
700
+ 7. **Data Gaps & Next Steps**
701
+ -
702
+ -
703
+ -
704
+
705
+ ---
706
+
707
+ 📦 RAW DATA:
708
+ {overall_answers}
709
+ """
710
+
711
+
712
+
713
+
714
+
715
+ formal_questions = [
716
+ {
717
+ "section": "Core Audience",
718
+ "questions": f"Who is {chosen_artist}'s core audience?",
719
+ "features": "include table breakdown of core audience"
720
+ }
721
+ ]
722
+
723
+ ##cut down Thought input, so only last one returned with Answer from
724
+
725
+
726
+ #count tokens anyway, for later usage:
727
+ total_tokens = count_tokens(prompto3)
728
+ print(f"Total tokens of prompt: {total_tokens}")
729
+
730
+ max_tokens = 16384 - total_tokens - 200
731
+
732
+ response = openai.chat.completions.create(
733
+ model="gpt-3.5-turbo-0125", # or "o3-pro" if enabled
734
+ messages=[
735
+ {
736
+ "role": "system",
737
+ "content": "You are a precise music industry data analyst. Be structured, factual, and preserve all stats given."
738
+ },
739
+ {
740
+ "role": "user",
741
+ "content": prompto3
742
+ }
743
+ ],
744
+ temperature=0.2, # low temp helps avoid paraphrasing
745
+ max_tokens=4096 # adjust based on your expected output size
746
+ )
747
+
748
+ print(response.choices[0].message.content)
749
+ two_pager_document = response.choices[0].message.content
750
+
751
+ #add generation of two_pager_part2
752
+
753
+
754
+ #for question in formal_questions:
755
+ print(f"overall_answer2 is {overall_answers2}")
756
+
757
+ with open(f"{chosen_artist}o32.txt","w", encoding="utf-8") as file:
758
+ file.write(two_pager_document)
759
+
760
+ return two_pager_document
761
+
762
+
763
+ demo = gr.Interface(
764
+ fn=main,
765
+ inputs="text",
766
+ outputs="text",
767
+ title="artist report generator",
768
+ description="generate report for artist"
769
+ )
770
+
771
+ demo.launch(share=True)
772
+
773
+ #if __name__ == "__main__":
774
+ #response = asyncio.run(main())
775
+ #then pass to llm to assemble formal response to formal questions
776
+
777
+ # FunctionAgent works for LLMs with a function calling API.
778
+ # ReActAgent works for any LLM.
779
+
780
+
781
+
782
+ #can check logs:
783
+ #async for ev in handler.stream_events():
784
+ #if isinstance(ev, ToolCallResult):
785
+ #print("")
786
+ #print("Called tool: ", ev.tool_name, ev.tool_kwargs, "=>", ev.tool_output)
787
+ #elif isinstance(ev, AgentStream): # showing the thought process
788
+ #print(ev.delta, end="", flush=True)
789
+