| Server IP : 3.147.158.171 / Your IP : 216.73.216.88 Web Server : Apache/2.4.67 (Amazon Linux) OpenSSL/3.5.5 System : Linux ip-172-31-2-178.us-east-2.compute.internal 6.1.172-216.329.amzn2023.x86_64 #1 SMP PREEMPT_DYNAMIC Wed May 20 06:31:34 UTC 2026 x86_64 User : ec2-user ( 1000) PHP Version : 8.4.21 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /tsai/www/html/api.turmansolutions.ai/app/services/ |
Upload File : |
import json
import logging
from typing import Any, Dict, List, Optional
from bs4 import BeautifulSoup
from app.appTypes import Character
from app.services.chat import ChatService
from app.services.wp import WpService
chat_service = ChatService()
wp_service = WpService()
SEARCH_CONTENT_TRUNCATE = 500
SEARCH_USAGE_NOTE = (
'Use these sources for accurate context. Quote sparingly. '
'Do not invent claims that are not supported by these sources or the article.'
)
FACT_CHECK_INSTRUCTION = (
'An editorial fact-check on this article is included below. It may flag '
'inaccuracies, missing context, or disputed claims. Be aware of it when '
'you write: do not repeat claims the fact-check has called into question, '
'and you may acknowledge or address concerns in your own voice if it fits '
'naturally. Stay in character — do not impersonate the fact-checker or '
'quote it verbatim.\n\n'
)
STYLE_GUIDE_COMMENT = (
'Write a comment that is easy to read, concise, and poignant.\n\n'
'Length and rhythm:\n'
'- Aim for 3 to 5 short paragraphs. A single sharp paragraph is also fine '
'when one point is all you have.\n'
'- Keep sentences short. Break long thoughts into separate sentences '
'instead of stacking clauses with dashes, commas, and "and." If a '
'sentence runs past about 25 words, split it.\n'
'- Vary sentence length to give the comment rhythm.\n\n'
'Formatting:\n'
'- Plain prose is the default. You may use a short bulleted or numbered '
'list (3-5 items) when you are genuinely enumerating things, but do not '
'force one.\n'
'- You may use light inline emphasis with *italics* or **bold**, and '
'`code` for short technical terms or names of things. Block quotes are '
'fine when quoting the article or another commenter.\n'
'- Do not use headings.\n\n'
'Voice:\n'
'- Stay in character. Match the tone of the article — playful where it '
"is playful, serious where it is serious. Do not assume the topic is "
'technical.\n'
'- Make a real point: react, add a perspective, ask a sharp question, '
'push back, or share a small relevant detail. Avoid generic praise and '
'avoid summarizing the article back at the reader.\n\n'
'Emojis:\n'
'- You may use emojis sparingly to add warmth or punctuate a beat — '
'generally zero, one, or at most two in a whole comment. Never start '
'with one, never decorate every paragraph, and never use them as bullet '
'markers.\n\n'
)
STYLE_GUIDE_REPLY = (
'Write a reply that is easy to read, concise, and poignant.\n\n'
'Length and rhythm:\n'
'- Aim for 1 to 3 short paragraphs. A single sharp paragraph is often '
'best for a reply.\n'
'- Keep sentences short. Break long thoughts into separate sentences '
'instead of stacking clauses with dashes, commas, and "and." If a '
'sentence runs past about 25 words, split it.\n'
'- Vary sentence length to give the reply rhythm.\n\n'
'Formatting:\n'
'- Plain prose is the default. A short bulleted or numbered list (3-5 '
'items) is fine only when you are genuinely enumerating things; most '
'replies should not have one.\n'
'- You may use light inline emphasis with *italics* or **bold**, and '
'`code` for short technical terms or names of things. Block quotes are '
'fine when quoting the article or another commenter.\n'
'- Do not use headings.\n\n'
'Voice:\n'
'- Stay in character. Engage directly with the person you are replying '
'to — agree, push back, clarify, or build on what they said. Do not '
'assume the topic is technical.\n'
'- Make a real point. Avoid generic agreement and avoid restating their '
'comment back at them.\n\n'
'Emojis:\n'
'- You may use emojis sparingly to add warmth or punctuate a beat — '
'generally zero or one in a reply. Never start with one and never use '
'them as bullet markers.\n\n'
)
def _format_fact_check(fact_check_comment: Optional[Dict[str, Any]]) -> str:
if not fact_check_comment:
return ''
rendered = fact_check_comment.get('content', {}).get('rendered', '')
text = BeautifulSoup(rendered, 'html.parser').get_text(separator='\n').strip()
if not text:
return ''
author = fact_check_comment.get('author_name', 'Fact-check')
return (
'\n--- Editorial fact-check on this article ---\n'
f'(posted by {author})\n\n'
f'{text}\n'
'--- end fact-check ---\n\n'
)
def _format_search_results(article: Dict[str, Any]) -> str:
meta = article.get('meta') or {}
raw = meta.get('_tsai_search_results') if isinstance(meta, dict) else None
if not raw:
return ''
try:
results = json.loads(raw)
except (TypeError, ValueError):
logging.debug('Could not parse _tsai_search_results meta')
return ''
if not results:
return ''
lines = ['', 'Research sources used to write this article:', '']
for i, r in enumerate(results, 1):
title = r.get('title') or 'Untitled'
url = r.get('url') or ''
content = (r.get('content') or '').strip()
if len(content) > SEARCH_CONTENT_TRUNCATE:
content = content[:SEARCH_CONTENT_TRUNCATE].rstrip() + '…'
lines.append(f'{i}. {title}')
if url:
lines.append(f' {url}')
if content:
lines.append(f' {content}')
lines.append('')
lines.append(SEARCH_USAGE_NOTE)
return '\n'.join(lines) + '\n\n'
class CommentsService:
def __init__(self):
pass
def get_comment_instructions(
self, character: Character, user_instructions, has_fact_check: bool = False
) -> str:
instructions = 'You need to provide a comment for an article.\n\n'
if user_instructions:
instructions += user_instructions + '\n\n'
else:
instructions += f'Your name is {character.name}. A good description of you would be: \n\n'
instructions += character.description
instructions += '\n\n'
if has_fact_check:
instructions += FACT_CHECK_INSTRUCTION
instructions += STYLE_GUIDE_COMMENT
logging.debug('comment instructions:\n%s', instructions)
return instructions
def get_comment_content(self, title: str, article_content: str) -> str:
content = f'Below is an article for you to comment on. The name of the article is: {title}. \n\n'
content += article_content + '\n\n'
logging.debug('comment msg:\n%s', content)
return content
def get_comment_reply_instructions(
self, character: Character, user_instructions,
is_author: bool = False, has_fact_check: bool = False,
) -> str:
instructions = 'You need to participate in a discussion about an article.\n\n'
if user_instructions:
instructions += user_instructions + '\n\n'
else:
instructions += f'Your name is {character.name}. A good description of you would be: \n\n'
instructions += character.description
instructions += '\n\n'
if is_author:
instructions += (
'IMPORTANT: You are the author of the article below. When you reply, '
'speak as the author of the piece. Do not summarize or critique the article '
'as if you were a reader; instead, engage directly with the commenter — '
'clarify your reasoning, expand on points they raised, acknowledge good '
'questions, or push back where you disagree.\n\n'
)
if has_fact_check:
instructions += FACT_CHECK_INSTRUCTION
instructions += STYLE_GUIDE_REPLY
logging.debug('comment reply instructions:\n%s', instructions)
return instructions
def _is_article_author(self, character: Character, article: Dict[str, Any]) -> bool:
article_author_id = article.get('author')
if character.wp_user and article_author_id is not None:
if str(article_author_id) == str(character.wp_user.id):
return True
embedded_author = (article.get('_embedded', {}).get('author') or [{}])[0]
embedded_name = embedded_author.get('name', '')
if embedded_name and embedded_name.strip().lower() == character.name.strip().lower():
return True
return False
def get_comment_reply_content(self, article_title: str, article_content: str, comment_thread: List[Dict[str, Any]]) -> str:
msg = 'Below is an article with some comments underneath. Please provide a reply to the last comment shown.\n\n'
msg += f'The name of the article is: {article_title}. \n\n'
msg += 'Article\n\n'
msg += article_content
msg += '\n\nComments \n\n'
# Reverse thread so it shows oldest to newest (chronological order)
# This way "last comment" is the actual target for reply
reversed_thread = list(reversed(comment_thread))
for comment in reversed_thread:
comment_author = comment['author_name']
comment_soup = BeautifulSoup(
comment['content']['rendered'], 'html.parser')
comment_body = comment_soup.get_text(separator='\n')
msg += f'{comment_author}: {comment_body}\n\n\n'
logging.debug('comment reply msg:\n%s', msg)
return msg
def compose_comment(
self,
character: Character,
article: Dict[str, Any],
images: List[str],
user_instructions: Optional[str] = None,
fact_check_comment: Optional[Dict[str, Any]] = None,
) -> str:
"""Build prompt, call the LLM, return rendered comment HTML."""
title = article.get('title', {}).get('rendered', '')
article_content = article.get('content', {}).get('rendered', '')
instructions = self.get_comment_instructions(
character, user_instructions, has_fact_check=bool(fact_check_comment),
)
content = self.get_comment_content(title, article_content)
content += _format_search_results(article)
content += _format_fact_check(fact_check_comment)
return chat_service.fetch_comment(instructions, content, images, model=character.model)
def compose_reply(
self,
character: Character,
article: Dict[str, Any],
target_comment: Dict[str, Any],
all_comments: List[Dict[str, Any]],
images: List[str],
user_instructions: Optional[str] = None,
skip_self_reply: bool = True,
fact_check_comment: Optional[Dict[str, Any]] = None,
) -> Optional[str]:
"""Build prompt, call the LLM, return rendered reply HTML.
Returns None when skip_self_reply is True and the target was authored by this character.
"""
if skip_self_reply and target_comment.get('author_name', '').lower() == character.name.lower():
logging.info('comment reply SKIPPED - would be replying to own comment')
return None
title = article.get('title', {}).get('rendered', '')
article_html = article.get('content', {}).get('rendered', '')
article_text = BeautifulSoup(article_html, 'html.parser').get_text(separator='\n')
comment_thread = wp_service.make_comment_thread(target_comment, all_comments)
logging.info(f'comment thread count is {len(comment_thread)}')
logging.info('Comment thread hierarchy (target -> oldest):')
for idx, c in enumerate(comment_thread):
indent = ' ' * idx
logging.info(
f'{indent}[{idx}] ID:{c["id"]} by {c["author_name"]} (parent:{c.get("parent", 0)})'
)
is_author = self._is_article_author(character, article)
if is_author:
logging.info(f'Reply context: {character.name} is the author of "{title}"')
instructions = self.get_comment_reply_instructions(
character, user_instructions, is_author=is_author,
has_fact_check=bool(fact_check_comment),
)
msg = self.get_comment_reply_content(title, article_text, comment_thread)
msg += _format_search_results(article)
msg += _format_fact_check(fact_check_comment)
return chat_service.fetch_comment(instructions, msg, images, model=character.model)