seawolf2357 commited on
Commit
fa891a4
Β·
verified Β·
1 Parent(s): b6d8ace

fix: tz-naive datetime crash + initial-backup safety + English-only sweep

Browse files
Files changed (1) hide show
  1. npc_core.py +40 -31
npc_core.py CHANGED
@@ -16,11 +16,11 @@ logger = logging.getLogger(__name__)
16
  KST = timezone(timedelta(hours=9))
17
 
18
  # ══════════════════════════════════════════════════
19
- # Global DB Connection Pool β€” database locked λ°©μ§€
20
  # ══════════════════════════════════════════════════
21
- _db_semaphore = asyncio.Semaphore(20) # 일반 DB μ—°κ²° (μŠ€μΌ€μ€„λŸ¬+importedν•¨μˆ˜)
22
- _db_write_lock = asyncio.Semaphore(2) # μ΅œλŒ€ 2개 λ™μ‹œ μ“°κΈ°
23
- _db_read_sem = asyncio.Semaphore(30) # API 읽기 μ „μš© β€” WAL은 λ¬΄μ œν•œ λ™μ‹œ 읽기 지원
24
 
25
  @asynccontextmanager
26
  async def get_db(db_path, write=False, timeout=30.0):
@@ -174,10 +174,10 @@ def get_wuxing_relation(identity_a: str, identity_b: str) -> str:
174
  wa = WUXING_MAP.get(identity_a, {})
175
  wb = WUXING_MAP.get(identity_b, {})
176
  if wa.get('generates') == identity_b or wb.get('generates') == identity_a:
177
- return '상생'
178
  if wa.get('overcomes') == identity_b or wb.get('overcomes') == identity_a:
179
- return '상극'
180
- return '쀑립'
181
  MEME_WORDS = {
182
  'suffix': ['lol', 'fr fr', 'no cap', 'facts', 'tbh', 'ngl', 'ong'],
183
  'slang': ['lit', 'fire', 'goated', 'vibes', 'mid', 'rizz', 'lowkey', 'highkey', 'no cap'],
@@ -266,12 +266,12 @@ async def quick_brave_verify(claim: str, brave_api_key: str = None, max_results:
266
  return {'verified': False, 'sources': [], 'warning': 'Fact-check unavailable'}
267
  def build_metacognition_prompt(agent_identity: str, post_author_identity: str = '',
268
  action: str = 'comment', fact_context: str = '') -> str:
269
- relation = get_wuxing_relation(agent_identity, post_author_identity) if post_author_identity else '쀑립'
270
- if relation == '상극':
271
  relation_instruction = """βš”οΈ [COUNTER-CHECK MODE] You fundamentally DISAGREE with this perspective.
272
  Find the weakest factual claim and challenge it with evidence. Be constructively critical.
273
  Your role is to prevent groupthink and hallucination in this community."""
274
- elif relation == '상생':
275
  relation_instruction = """🀝 [SYNERGY MODE] You RESONATE with this perspective.
276
  Add a complementary insight the original author missed. Build on their idea with NEW information.
277
  Your role is to amplify good ideas and create emergent value."""
@@ -776,7 +776,11 @@ GOOD EXAMPLES:
776
  if fact_check.get('fact_check_results'):
777
  fact_sources = ' | '.join([fc['title'][:60] for fc in fact_check['fact_check_results'][:2]])
778
  post_meta = build_metacognition_prompt(ai_identity_key, '', 'post', fact_sources)
 
 
779
  prompt = f"""You are {agent['username']} ({mbti}) - {identity['name']}: "{identity['core_belief']}"
 
 
780
  πŸ”΄ CRITICAL RULES:
781
  1. Write ONLY in English
782
  2. Based on REAL latest news/trends provided below
@@ -806,7 +810,7 @@ LENGTH: 1000-1500 chars
806
  【OUTPUT FORMAT】
807
  Title: (15-20 words, hook that makes people click)
808
  Body: (post with TL;DR)
809
- Write in English ONLY:"""
810
  response = await self.ai.create_chat_completion([{"role": "user", "content": prompt}], temperature=0.95, max_tokens=3500)
811
  if not response or len(response) < 200:
812
  result = self._create_emergency_reddit_post(topic, identity, research_data, agent.get('agent_id', ''))
@@ -927,8 +931,8 @@ Write in English ONLY:"""
927
  ['personal_story', 'devil_advocate', 'tangent', 'real_world',
928
  'skeptic_question', 'humor_sarcasm', 'future_prediction', 'comparison'],
929
  weights=[0.18, 0.15, 0.13, 0.15, 0.12, 0.10, 0.09, 0.08])[0]
930
- relation = get_wuxing_relation(agent_identity, post_author_identity) if post_author_identity else '쀑립'
931
- if relation == '상극':
932
  angle = random.choices(
933
  ['devil_advocate', 'skeptic_question', 'real_world', 'comparison'],
934
  weights=[0.35, 0.30, 0.20, 0.15])[0]
@@ -944,7 +948,10 @@ Write in English ONLY:"""
944
  fc = await quick_brave_verify(core_claim)
945
  if fc['sources']: fact_context = ' | '.join(fc['sources'][:2])
946
  meta_prompt = build_metacognition_prompt(agent_identity, post_author_identity, 'comment', fact_context)
947
- prompt = f"""Read this post and write ONE comment as a real human.
 
 
 
948
  Today's date: {today_str}
949
  --- POST ---
950
  {post_content}
@@ -958,7 +965,8 @@ RULES:
958
  - NEVER claim you attended, visited, or experienced events mentioned in the post.
959
  - If the post mentions a FUTURE event (after {today_str}), do NOT write as if it already happened.
960
  - Stay on the post's ACTUAL topic.
961
- - You MUST end with a complete sentence.{user_inst}
 
962
  Comment:"""
963
  temp = random.uniform(0.88, 1.20)
964
  comment = await self.ai.create_chat_completion([{"role": "user", "content": prompt}], temperature=temp, max_tokens=350)
@@ -1059,7 +1067,7 @@ class NPCStrategy:
1059
  cursor = await db.execute("SELECT p.id, COALESCE(a.ai_identity,'') FROM posts p LEFT JOIN npc_agents a ON p.author_agent_id=a.agent_id WHERE NOT EXISTS (SELECT 1 FROM likes l WHERE l.agent_id=? AND l.target_type='post' AND l.target_id=p.id) ORDER BY RANDOM() LIMIT 20", (agent['agent_id'],))
1060
  posts = await cursor.fetchall()
1061
  if not posts: return None
1062
- synergy_posts = [p for p in posts if p[1] and get_wuxing_relation(agent_identity, p[1]) == '상생']
1063
  if synergy_posts and random.random() < 0.7: return random.choice(synergy_posts)[0]
1064
  return random.choice(posts)[0]
1065
  @staticmethod
@@ -1074,7 +1082,7 @@ class NPCStrategy:
1074
  """, (agent['agent_id'],))
1075
  posts = await cursor.fetchall()
1076
  if not posts: return None
1077
- counter_posts = [p for p in posts if p[3] and get_wuxing_relation(ai_identity, p[3]) == '상극']
1078
  if counter_posts and random.random() < 0.6: return random.choice(counter_posts)[0]
1079
  dislike_keywords = {
1080
  'doomer': ['hope', 'bright', 'positive', 'progress', 'growth', 'moon', 'bull'],
@@ -1297,18 +1305,18 @@ def build_chat_msg(msg_type, username, identity, style, ticker, price, chg, is_b
1297
  if msg_type == 'reply' and recent_msgs:
1298
  target = random.choice(recent_msgs)
1299
  target_identity = target[4] if len(target) > 4 else ''
1300
- relation = get_wuxing_relation(identity, target_identity) if target_identity else '쀑립'
1301
- if relation == '상생': agree = random.random() > 0.15
1302
- elif relation == '상극': agree = random.random() > 0.75
1303
  else: agree = random.random() > 0.4
1304
  t_ticker = target[3] or ticker
1305
- if relation == '상극':
1306
  msgs = [
1307
  f"@{target[1]} Hard disagree. My {style} analysis shows the OPPOSITE on {t_ticker}.",
1308
  f"@{target[1]} Where's the evidence? Your claim about {t_ticker} doesn't hold up πŸ”",
1309
  f"@{target[1]} {'Interesting take but' if agree else 'Exactly the groupthink that'} gets traders {'thinking' if agree else 'wrecked'} ⚠️",
1310
  f"@{target[1]} I checked the data β€” {'you might be onto something' if agree else 'numbers tell a different story'} {emoji}",]
1311
- elif relation == '상생':
1312
  msgs = [
1313
  f"@{target[1]} Great minds! My {style} view supports this on {t_ticker} {emoji}",
1314
  f"@{target[1]} Building on your point β€” the {style} signal also confirms momentum 🀝",
@@ -1374,7 +1382,7 @@ IDENTITY_RESEARCH_STYLE = {
1374
  def build_research_report(agent_id, username, identity, mbti, ticker, company,
1375
  price, change_pct, rsi, pe, from_high, mcap,
1376
  win_rate, total_trades, total_pnl, style_info):
1377
- """β˜… 심측 λ¦¬μ„œμΉ˜ λ³΄κ³ μ„œ 생성 (탄λ ₯μ„± 계산 포함)"""
1378
  is_bearish = identity in ['doomer', 'skeptic']
1379
  is_bullish = identity in ['revolutionary', 'creative', 'transcendent']
1380
  is_quant = identity in ['scientist', 'awakened']
@@ -1568,7 +1576,7 @@ def build_research_report(agent_id, username, identity, mbti, ticker, company,
1568
  # Part 7: Research Generation + Auto Purchase
1569
  # ────���────────────────────────────────────────
1570
  async def generate_npc_research_reports(db_path: str, ai_client=None):
1571
- """Top 30 NPCκ°€ 랜덀 티컀에 λŒ€ν•΄ 심측 λ¦¬μ„œμΉ˜ 생산"""
1572
  try:
1573
  async with get_db(db_path) as db:
1574
  cursor = await db.execute("""
@@ -1632,7 +1640,7 @@ async def generate_npc_research_reports(db_path: str, ai_client=None):
1632
  except Exception as e:
1633
  logger.error(f"Research generation error: {e}")
1634
  async def npc_auto_purchase_research(db_path: str):
1635
- """NPCκ°€ μžλ™μœΌλ‘œ κ³ ν’ˆμ§ˆ λ¦¬μ„œμΉ˜ ꡬ맀"""
1636
  try:
1637
  async with get_db(db_path) as db:
1638
  cursor = await db.execute("""
@@ -1665,7 +1673,7 @@ async def npc_auto_purchase_research(db_path: str):
1665
  except Exception as e:
1666
  logger.error(f"Auto purchase research error: {e}")
1667
  async def bootstrap_research_reports(db_path: str):
1668
- """β˜… μ„œλ²„ λΆ€νŒ… μ‹œ Research Reportsκ°€ 0건이면 μ¦‰μ‹œ 3건 생성 + λ‚˜λ¨Έμ§€ λ°±κ·ΈλΌμš΄λ“œ"""
1669
  try:
1670
  async with get_db(db_path) as db:
1671
  cursor = await db.execute("SELECT COUNT(*) FROM npc_research_reports")
@@ -1724,7 +1732,7 @@ async def bootstrap_research_reports(db_path: str):
1724
  logger.error(f"πŸ”¬ Research bootstrap error: {e}")
1725
  import traceback; traceback.print_exc()
1726
  async def background_research_fill(db_path: str, tickers, npcs, start_npc_idx):
1727
- """β˜… λ‚˜λ¨Έμ§€ 티컀에 λŒ€ν•΄ λ°±κ·ΈλΌμš΄λ“œλ‘œ λ¦¬μ„œμΉ˜ 리포트 순차 생성"""
1728
  await asyncio.sleep(10)
1729
  generated = 0
1730
  for i, ticker in enumerate(tickers):
@@ -1765,7 +1773,7 @@ async def background_research_fill(db_path: str, tickers, npcs, start_npc_idx):
1765
  async def generate_npc_comment_replies(db_path: str, groq_api_key: str,
1766
  post_id: int, post_title: str, post_content: str,
1767
  user_comment: str, user_name: str, parent_id: int):
1768
- """μœ μ € λŒ“κΈ€μ— NPC 1~5λͺ… λŒ€λŒ“κΈ€ β€” AETHER-Lite 메타인지 적용"""
1769
  try:
1770
  ai = GroqAIClient(groq_api_key)
1771
  npc_count = random.randint(1, 5)
@@ -1795,10 +1803,11 @@ Post title: "{post_title[:100]}"
1795
  User @{user_name} commented: "{user_comment[:300]}"
1796
  {meta}
1797
  Write a short reply (1-3 sentences). Be opinionated and stay in character.
 
1798
  RULES:
1799
  - Do NOT make up statistics or facts you cannot verify.
1800
  - If you challenge a claim, explain WHY with reasoning.
1801
- - If the user wrote in Korean, reply in Korean. If in English, reply in English.
1802
  Reply ONLY with the message text."""
1803
  reply = await ai.create_chat_completion(
1804
  [{"role": "user", "content": prompt}], max_tokens=256, temperature=0.9)
@@ -1817,7 +1826,7 @@ Reply ONLY with the message text."""
1817
  async def generate_npc_chat_replies_to_user(db_path: str, groq_api_key: str,
1818
  user_message: str, user_username: str,
1819
  user_msg_id: int, npcs: list):
1820
- """μœ μ € λ©”μ‹œμ§€μ— λŒ€ν•΄ NPC듀이 캐릭터에 λ§žλŠ” λ°˜μ‘ 생성"""
1821
  try:
1822
  ai = GroqAIClient(groq_api_key)
1823
  async with get_db(db_path) as db:
@@ -1827,7 +1836,7 @@ async def generate_npc_chat_replies_to_user(db_path: str, groq_api_key: str,
1827
  prompt = f"""You are {npc_username}, an NPC trader with {identity} personality and {mbti} MBTI type in a trading community chat.
1828
  A human user @{user_username} just said: "{user_message}"
1829
  Reply naturally in 1-3 sentences as your character. Be engaging, opinionated, and stay in character.
1830
- If the user wrote in Korean, reply in Korean. If in English, reply in English.
1831
  Reply ONLY with the message text, nothing else."""
1832
  reply = await ai.create_chat_completion(
1833
  [{"role": "user", "content": prompt}], max_tokens=512, temperature=0.9)
 
16
  KST = timezone(timedelta(hours=9))
17
 
18
  # ══════════════════════════════════════════════════
19
+ # Global DB Connection Pool β€” prevents database locked
20
  # ══════════════════════════════════════════════════
21
+ _db_semaphore = asyncio.Semaphore(20) # General DB connections (scheduler + imported functions)
22
+ _db_write_lock = asyncio.Semaphore(2) # Max 2 concurrent writes
23
+ _db_read_sem = asyncio.Semaphore(30) # API read-only β€” WAL supports unlimited concurrent reads
24
 
25
  @asynccontextmanager
26
  async def get_db(db_path, write=False, timeout=30.0):
 
174
  wa = WUXING_MAP.get(identity_a, {})
175
  wb = WUXING_MAP.get(identity_b, {})
176
  if wa.get('generates') == identity_b or wb.get('generates') == identity_a:
177
+ return 'synergy'
178
  if wa.get('overcomes') == identity_b or wb.get('overcomes') == identity_a:
179
+ return 'conflict'
180
+ return 'neutral'
181
  MEME_WORDS = {
182
  'suffix': ['lol', 'fr fr', 'no cap', 'facts', 'tbh', 'ngl', 'ong'],
183
  'slang': ['lit', 'fire', 'goated', 'vibes', 'mid', 'rizz', 'lowkey', 'highkey', 'no cap'],
 
266
  return {'verified': False, 'sources': [], 'warning': 'Fact-check unavailable'}
267
  def build_metacognition_prompt(agent_identity: str, post_author_identity: str = '',
268
  action: str = 'comment', fact_context: str = '') -> str:
269
+ relation = get_wuxing_relation(agent_identity, post_author_identity) if post_author_identity else 'neutral'
270
+ if relation == 'conflict':
271
  relation_instruction = """βš”οΈ [COUNTER-CHECK MODE] You fundamentally DISAGREE with this perspective.
272
  Find the weakest factual claim and challenge it with evidence. Be constructively critical.
273
  Your role is to prevent groupthink and hallucination in this community."""
274
+ elif relation == 'synergy':
275
  relation_instruction = """🀝 [SYNERGY MODE] You RESONATE with this perspective.
276
  Add a complementary insight the original author missed. Build on their idea with NEW information.
277
  Your role is to amplify good ideas and create emergent value."""
 
776
  if fact_check.get('fact_check_results'):
777
  fact_sources = ' | '.join([fc['title'][:60] for fc in fact_check['fact_check_results'][:2]])
778
  post_meta = build_metacognition_prompt(ai_identity_key, '', 'post', fact_sources)
779
+ KST = timezone(timedelta(hours=9))
780
+ current_kst = datetime.now(KST).strftime("%Y-%m-%d %H:%M (KST)")
781
  prompt = f"""You are {agent['username']} ({mbti}) - {identity['name']}: "{identity['core_belief']}"
782
+ [Current time] {current_kst}
783
+ * Be aware of the current date and time, and reflect it naturally when relevant.
784
  πŸ”΄ CRITICAL RULES:
785
  1. Write ONLY in English
786
  2. Based on REAL latest news/trends provided below
 
810
  【OUTPUT FORMAT】
811
  Title: (15-20 words, hook that makes people click)
812
  Body: (post with TL;DR)
813
+ Write in English:"""
814
  response = await self.ai.create_chat_completion([{"role": "user", "content": prompt}], temperature=0.95, max_tokens=3500)
815
  if not response or len(response) < 200:
816
  result = self._create_emergency_reddit_post(topic, identity, research_data, agent.get('agent_id', ''))
 
931
  ['personal_story', 'devil_advocate', 'tangent', 'real_world',
932
  'skeptic_question', 'humor_sarcasm', 'future_prediction', 'comparison'],
933
  weights=[0.18, 0.15, 0.13, 0.15, 0.12, 0.10, 0.09, 0.08])[0]
934
+ relation = get_wuxing_relation(agent_identity, post_author_identity) if post_author_identity else 'neutral'
935
+ if relation == 'conflict':
936
  angle = random.choices(
937
  ['devil_advocate', 'skeptic_question', 'real_world', 'comparison'],
938
  weights=[0.35, 0.30, 0.20, 0.15])[0]
 
948
  fc = await quick_brave_verify(core_claim)
949
  if fc['sources']: fact_context = ' | '.join(fc['sources'][:2])
950
  meta_prompt = build_metacognition_prompt(agent_identity, post_author_identity, 'comment', fact_context)
951
+ KST = timezone(timedelta(hours=9))
952
+ current_kst = datetime.now(KST).strftime("%Y-%m-%d %H:%M (KST)")
953
+ prompt = f"""Read this post and write ONE comment as a real human. Write in English.
954
+ Current time: {current_kst}
955
  Today's date: {today_str}
956
  --- POST ---
957
  {post_content}
 
965
  - NEVER claim you attended, visited, or experienced events mentioned in the post.
966
  - If the post mentions a FUTURE event (after {today_str}), do NOT write as if it already happened.
967
  - Stay on the post's ACTUAL topic.
968
+ - You MUST end with a complete sentence.
969
+ - Write in English.{user_inst}
970
  Comment:"""
971
  temp = random.uniform(0.88, 1.20)
972
  comment = await self.ai.create_chat_completion([{"role": "user", "content": prompt}], temperature=temp, max_tokens=350)
 
1067
  cursor = await db.execute("SELECT p.id, COALESCE(a.ai_identity,'') FROM posts p LEFT JOIN npc_agents a ON p.author_agent_id=a.agent_id WHERE NOT EXISTS (SELECT 1 FROM likes l WHERE l.agent_id=? AND l.target_type='post' AND l.target_id=p.id) ORDER BY RANDOM() LIMIT 20", (agent['agent_id'],))
1068
  posts = await cursor.fetchall()
1069
  if not posts: return None
1070
+ synergy_posts = [p for p in posts if p[1] and get_wuxing_relation(agent_identity, p[1]) == 'synergy']
1071
  if synergy_posts and random.random() < 0.7: return random.choice(synergy_posts)[0]
1072
  return random.choice(posts)[0]
1073
  @staticmethod
 
1082
  """, (agent['agent_id'],))
1083
  posts = await cursor.fetchall()
1084
  if not posts: return None
1085
+ counter_posts = [p for p in posts if p[3] and get_wuxing_relation(ai_identity, p[3]) == 'conflict']
1086
  if counter_posts and random.random() < 0.6: return random.choice(counter_posts)[0]
1087
  dislike_keywords = {
1088
  'doomer': ['hope', 'bright', 'positive', 'progress', 'growth', 'moon', 'bull'],
 
1305
  if msg_type == 'reply' and recent_msgs:
1306
  target = random.choice(recent_msgs)
1307
  target_identity = target[4] if len(target) > 4 else ''
1308
+ relation = get_wuxing_relation(identity, target_identity) if target_identity else 'neutral'
1309
+ if relation == 'synergy': agree = random.random() > 0.15
1310
+ elif relation == 'conflict': agree = random.random() > 0.75
1311
  else: agree = random.random() > 0.4
1312
  t_ticker = target[3] or ticker
1313
+ if relation == 'conflict':
1314
  msgs = [
1315
  f"@{target[1]} Hard disagree. My {style} analysis shows the OPPOSITE on {t_ticker}.",
1316
  f"@{target[1]} Where's the evidence? Your claim about {t_ticker} doesn't hold up πŸ”",
1317
  f"@{target[1]} {'Interesting take but' if agree else 'Exactly the groupthink that'} gets traders {'thinking' if agree else 'wrecked'} ⚠️",
1318
  f"@{target[1]} I checked the data β€” {'you might be onto something' if agree else 'numbers tell a different story'} {emoji}",]
1319
+ elif relation == 'synergy':
1320
  msgs = [
1321
  f"@{target[1]} Great minds! My {style} view supports this on {t_ticker} {emoji}",
1322
  f"@{target[1]} Building on your point β€” the {style} signal also confirms momentum 🀝",
 
1382
  def build_research_report(agent_id, username, identity, mbti, ticker, company,
1383
  price, change_pct, rsi, pe, from_high, mcap,
1384
  win_rate, total_trades, total_pnl, style_info):
1385
+ """β˜… Generate deep research report (includes elasticity calculation)"""
1386
  is_bearish = identity in ['doomer', 'skeptic']
1387
  is_bullish = identity in ['revolutionary', 'creative', 'transcendent']
1388
  is_quant = identity in ['scientist', 'awakened']
 
1576
  # Part 7: Research Generation + Auto Purchase
1577
  # ────���────────────────────────────────────────
1578
  async def generate_npc_research_reports(db_path: str, ai_client=None):
1579
+ """Top 30 NPCs produce deep research on random tickers"""
1580
  try:
1581
  async with get_db(db_path) as db:
1582
  cursor = await db.execute("""
 
1640
  except Exception as e:
1641
  logger.error(f"Research generation error: {e}")
1642
  async def npc_auto_purchase_research(db_path: str):
1643
+ """NPCs automatically purchase high-quality research"""
1644
  try:
1645
  async with get_db(db_path) as db:
1646
  cursor = await db.execute("""
 
1673
  except Exception as e:
1674
  logger.error(f"Auto purchase research error: {e}")
1675
  async def bootstrap_research_reports(db_path: str):
1676
+ """β˜… On server boot, generate 3 research reports immediately if 0 exist + rest in background"""
1677
  try:
1678
  async with get_db(db_path) as db:
1679
  cursor = await db.execute("SELECT COUNT(*) FROM npc_research_reports")
 
1732
  logger.error(f"πŸ”¬ Research bootstrap error: {e}")
1733
  import traceback; traceback.print_exc()
1734
  async def background_research_fill(db_path: str, tickers, npcs, start_npc_idx):
1735
+ """β˜… Sequentially generate research reports in background for remaining tickers"""
1736
  await asyncio.sleep(10)
1737
  generated = 0
1738
  for i, ticker in enumerate(tickers):
 
1773
  async def generate_npc_comment_replies(db_path: str, groq_api_key: str,
1774
  post_id: int, post_title: str, post_content: str,
1775
  user_comment: str, user_name: str, parent_id: int):
1776
+ """1~5 NPCs reply to user's comment β€” AETHER-Lite metacognition applied"""
1777
  try:
1778
  ai = GroqAIClient(groq_api_key)
1779
  npc_count = random.randint(1, 5)
 
1803
  User @{user_name} commented: "{user_comment[:300]}"
1804
  {meta}
1805
  Write a short reply (1-3 sentences). Be opinionated and stay in character.
1806
+ Reply in English only.
1807
  RULES:
1808
  - Do NOT make up statistics or facts you cannot verify.
1809
  - If you challenge a claim, explain WHY with reasoning.
1810
+ - Reply in English only.
1811
  Reply ONLY with the message text."""
1812
  reply = await ai.create_chat_completion(
1813
  [{"role": "user", "content": prompt}], max_tokens=256, temperature=0.9)
 
1826
  async def generate_npc_chat_replies_to_user(db_path: str, groq_api_key: str,
1827
  user_message: str, user_username: str,
1828
  user_msg_id: int, npcs: list):
1829
+ """NPCs generate in-character reactions to a user message"""
1830
  try:
1831
  ai = GroqAIClient(groq_api_key)
1832
  async with get_db(db_path) as db:
 
1836
  prompt = f"""You are {npc_username}, an NPC trader with {identity} personality and {mbti} MBTI type in a trading community chat.
1837
  A human user @{user_username} just said: "{user_message}"
1838
  Reply naturally in 1-3 sentences as your character. Be engaging, opinionated, and stay in character.
1839
+ Reply in English only.
1840
  Reply ONLY with the message text, nothing else."""
1841
  reply = await ai.create_chat_completion(
1842
  [{"role": "user", "content": prompt}], max_tokens=512, temperature=0.9)