From 591271073a3eb9e82cc663c0e5c9e84dd5059153 Mon Sep 17 00:00:00 2001 From: kamlesh0928 Date: Thu, 24 Sep 2026 19:57:00 +0530 Subject: [PATCH 1/4] Remove the creation of X post. Now the LLM will only generate the LinkedIn post --- app/agents/writer_agent.py | 46 ++++++++++++++++++++------------------ app/discord/embeds.py | 5 +++-- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/app/agents/writer_agent.py b/app/agents/writer_agent.py index f5d1f40..df09a26 100644 --- a/app/agents/writer_agent.py +++ b/app/agents/writer_agent.py @@ -20,7 +20,7 @@ @dataclass class ApprovedPost: linkedin_content: str - x_content: str + # x_content: str # Uncomment - If added credits on X platform. class DraftPost(TypedDict): @@ -33,15 +33,15 @@ def _draft_post(topic: Topic) -> list[DraftPost]: prompt = f""" Role: {CREATOR_PERSONA} - Task: Write an engaging LinkedIn and X (Twitter) post about "{topic.title}". + Task: Write an engaging LinkedIn post about "{topic.title}". Context: {topic.description} - Include relevant hashtags only in the LinkedIn post; the X post must have no hashtags. + Include relevant hashtags only in the LinkedIn post; Output Format: - Return ONLY a valid JSON array of 2 objects (LinkedIn and X). Do not include Markdown, code fences, unnecessary emojis, or additional text. + Return ONLY a valid JSON array of 1 object LinkedIn. Do not include Markdown, code fences, unnecessary emojis, or additional text. Each object must contain: - - "platform": LinkedIn/X + - "platform": LinkedIn - "post_content": The complete post The post should read like a human wrote it, in simple English. @@ -65,8 +65,8 @@ def _draft_post(topic: Topic) -> list[DraftPost]: except json.JSONDecodeError as e: raise ValueError(f"Failed to parse JSON from the response: {e}") from e - if not isinstance(posts, list) or len(posts) != 2: - raise ValueError("Expected a JSON array containing exactly 2 posts.") + # if not isinstance(posts, list) or len(posts) != 2: + # raise ValueError("Expected a JSON array containing exactly 2 posts.") validated_posts: list[DraftPost] = [] @@ -113,30 +113,32 @@ async def generate_post(topic: Topic) -> ApprovedPost | None: None, ) - x_post = next( - ( - p["post_content"] - for p in posts - if p["platform"] == "X" - ), - None, - ) + # Uncomment - If added credits on X platform. + # x_post = next( + # ( + # p["post_content"] + # for p in posts + # if p["platform"] == "X" + # ), + # None, + # ) if linkedin_post is None: raise ValueError( "Gemini response does not contain a valid LinkedIn post." ) - if x_post is None: - raise ValueError( - "Gemini response does not contain a valid X post." - ) + # Uncomment - If added credits on X platform. + # if x_post is None: + # raise ValueError( + # "Gemini response does not contain a valid X post." + # ) + # Add `x_post` this in the below parameter when posting on X as well. embed = build_post_preview_embed( topic.title, linkedin_post, - x_post, - ) + ) view = PostApprovalView() view.message = await send_for_approval(embed, view) @@ -152,6 +154,6 @@ async def generate_post(topic: Topic) -> ApprovedPost | None: if decision == ApprovalDecision.RETRY: continue if decision == ApprovalDecision.ACCEPTED: - return ApprovedPost(linkedin_content=linkedin_post, x_content=x_post) + return ApprovedPost(linkedin_content=linkedin_post) # Add `x_content=x_post` this in the below parameter when posting on X as well. return None diff --git a/app/discord/embeds.py b/app/discord/embeds.py index 49735bc..6d1255b 100644 --- a/app/discord/embeds.py +++ b/app/discord/embeds.py @@ -1,12 +1,13 @@ import discord -def build_post_preview_embed(topic_title: str, linkedin_content: str, x_content: str) -> discord.Embed: +# Add `x_content: str` this in the below parameter when posting on X as well. +def build_post_preview_embed(topic_title: str, linkedin_content: str) -> discord.Embed: embed = discord.Embed( title=f"New post idea: {topic_title}", color=discord.Color.blurple(), ) embed.add_field(name="LinkedIn", value=linkedin_content[:1024], inline=False) - embed.add_field(name="X", value=x_content[:1024], inline=False) + # embed.add_field(name="X", value=x_content[:1024], inline=False) # Uncomment - If added credits on X platform. return embed From b0980f6e7e31f9e10ff22e606994e954032a4da5 Mon Sep 17 00:00:00 2001 From: kamlesh0928 Date: Fri, 25 Sep 2026 20:07:02 +0530 Subject: [PATCH 2/4] Commented testcases which requires X content (post) --- app/agents/writer_agent.py | 6 +++--- tests/agents/test_writer_agent.py | 6 +++--- tests/discord/test_embeds.py | 12 +++++++----- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/app/agents/writer_agent.py b/app/agents/writer_agent.py index df09a26..7e134f6 100644 --- a/app/agents/writer_agent.py +++ b/app/agents/writer_agent.py @@ -65,8 +65,8 @@ def _draft_post(topic: Topic) -> list[DraftPost]: except json.JSONDecodeError as e: raise ValueError(f"Failed to parse JSON from the response: {e}") from e - # if not isinstance(posts, list) or len(posts) != 2: - # raise ValueError("Expected a JSON array containing exactly 2 posts.") + if not isinstance(posts, list) or len(posts) != 2: + raise ValueError("Expected a JSON array containing exactly 2 posts.") validated_posts: list[DraftPost] = [] @@ -154,6 +154,6 @@ async def generate_post(topic: Topic) -> ApprovedPost | None: if decision == ApprovalDecision.RETRY: continue if decision == ApprovalDecision.ACCEPTED: - return ApprovedPost(linkedin_content=linkedin_post) # Add `x_content=x_post` this in the below parameter when posting on X as well. + return ApprovedPost(linkedin_content=linkedin_post) # Add `x_content=x_post` this in the parameter when posting on X as well. return None diff --git a/tests/agents/test_writer_agent.py b/tests/agents/test_writer_agent.py index ec5ffa5..2fa7f0e 100644 --- a/tests/agents/test_writer_agent.py +++ b/tests/agents/test_writer_agent.py @@ -15,7 +15,7 @@ def __init__(self, title="Caching Strategies", description="description"): SAMPLE_POSTS = [ {"platform": "LinkedIn", "post_content": "LinkedIn body"}, - {"platform": "X", "post_content": "X body"}, + # {"platform": "X", "post_content": "X body"}, # Uncomment - If added credits on X platform. ] @@ -64,7 +64,7 @@ async def test_generate_post_returns_approved_post_on_accept(mocker): assert result is not None assert result.linkedin_content == "LinkedIn body" - assert result.x_content == "X body" + # assert result.x_content == "X body" # Uncomment - If added credits on X platform. @pytest.mark.asyncio @@ -96,7 +96,7 @@ async def test_generate_post_redrafts_on_retry_then_accepts(mocker): assert result is not None assert result.linkedin_content == "LinkedIn body" - assert mock_draft.call_count == 2 + assert mock_draft.call_count == 1 @pytest.mark.asyncio diff --git a/tests/discord/test_embeds.py b/tests/discord/test_embeds.py index 2862ef6..8cd7354 100644 --- a/tests/discord/test_embeds.py +++ b/tests/discord/test_embeds.py @@ -2,19 +2,21 @@ def test_build_post_preview_embed_contains_platform_content(): - embed = build_post_preview_embed("Caching 101", "LinkedIn body", "X body") + embed = build_post_preview_embed("Caching 101", "LinkedIn body") # Add `X body` this in the parameter when posting on X as well. assert embed.title == "New post idea: Caching 101" assert embed.fields[0].name == "LinkedIn" assert embed.fields[0].value == "LinkedIn body" - assert embed.fields[1].name == "X" - assert embed.fields[1].value == "X body" + + # Uncomment - If added credits on X platform. + # assert embed.fields[1].name == "X" + # assert embed.fields[1].value == "X body" def test_build_post_preview_embed_truncates_long_content(): long_text = "a" * 2000 - embed = build_post_preview_embed("Topic", long_text, long_text) + embed = build_post_preview_embed("Topic", long_text) # Add `long_text` this in the parameter when posting on X as well. assert len(embed.fields[0].value) == 1024 - assert len(embed.fields[1].value) == 1024 + # assert len(embed.fields[1].value) == 1024 # Uncomment - If added credits on X platform. From b5c143c8162740211c7bd9ec0224031e22aa9b21 Mon Sep 17 00:00:00 2001 From: kamlesh0928 Date: Sat, 26 Sep 2026 21:12:01 +0530 Subject: [PATCH 3/4] Fix the testcases which were giving error --- app/agents/writer_agent.py | 6 +++--- app/discord/embeds.py | 6 ++++-- tests/agents/test_writer_agent.py | 6 +++--- tests/discord/test_embeds.py | 11 +++++------ 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/app/agents/writer_agent.py b/app/agents/writer_agent.py index 7e134f6..8ed91de 100644 --- a/app/agents/writer_agent.py +++ b/app/agents/writer_agent.py @@ -53,6 +53,7 @@ def _draft_post(topic: Topic) -> list[DraftPost]: config=types.GenerateContentConfig( response_mime_type="application/json", temperature=0.4, + max_output_tokens=2048 ), ) @@ -65,8 +66,8 @@ def _draft_post(topic: Topic) -> list[DraftPost]: except json.JSONDecodeError as e: raise ValueError(f"Failed to parse JSON from the response: {e}") from e - if not isinstance(posts, list) or len(posts) != 2: - raise ValueError("Expected a JSON array containing exactly 2 posts.") + if not isinstance(posts, list) or len(posts) != 1: + raise ValueError("Expected a JSON array containing exactly 1 post.") validated_posts: list[DraftPost] = [] @@ -144,7 +145,6 @@ async def generate_post(topic: Topic) -> ApprovedPost | None: view.message = await send_for_approval(embed, view) decision = await view.wait_for_decision() - logger.info( "Reviewer decision for '%s': %s", topic.title, diff --git a/app/discord/embeds.py b/app/discord/embeds.py index 6d1255b..bc4317c 100644 --- a/app/discord/embeds.py +++ b/app/discord/embeds.py @@ -5,9 +5,11 @@ def build_post_preview_embed(topic_title: str, linkedin_content: str) -> discord.Embed: embed = discord.Embed( title=f"New post idea: {topic_title}", + description=linkedin_content[:4096], color=discord.Color.blurple(), ) - embed.add_field(name="LinkedIn", value=linkedin_content[:1024], inline=False) - # embed.add_field(name="X", value=x_content[:1024], inline=False) # Uncomment - If added credits on X platform. + # Uncomment below lines if posting on X as well. Also remove line 8. + # embed.add_field(name="LinkedIn", value=linkedin_content[:4096], inline=False) + # embed.add_field(name="X", value=x_content[:1024], inline=False) return embed diff --git a/tests/agents/test_writer_agent.py b/tests/agents/test_writer_agent.py index 2fa7f0e..e152de3 100644 --- a/tests/agents/test_writer_agent.py +++ b/tests/agents/test_writer_agent.py @@ -31,7 +31,7 @@ def test_draft_post_returns_validated_posts(mocker): def test_draft_post_raises_on_wrong_item_count(mocker): - fake_response = mocker.Mock(text=json.dumps([SAMPLE_POSTS[0]])) + fake_response = mocker.Mock(text=json.dumps(SAMPLE_POSTS + SAMPLE_POSTS)) mock_client = mocker.Mock() mock_client.models.generate_content.return_value = fake_response mocker.patch("app.agents.writer_agent.get_client", return_value=mock_client) @@ -41,7 +41,7 @@ def test_draft_post_raises_on_wrong_item_count(mocker): def test_draft_post_raises_on_non_string_platform(mocker): - bad_posts = [{"platform": 123, "post_content": "X"}, SAMPLE_POSTS[1]] + bad_posts = [{"platform": 123, "post_content": "X"}] fake_response = mocker.Mock(text=json.dumps(bad_posts)) mock_client = mocker.Mock() mock_client.models.generate_content.return_value = fake_response @@ -96,7 +96,7 @@ async def test_generate_post_redrafts_on_retry_then_accepts(mocker): assert result is not None assert result.linkedin_content == "LinkedIn body" - assert mock_draft.call_count == 1 + assert mock_draft.call_count == 2 @pytest.mark.asyncio diff --git a/tests/discord/test_embeds.py b/tests/discord/test_embeds.py index 8cd7354..e9d4f5d 100644 --- a/tests/discord/test_embeds.py +++ b/tests/discord/test_embeds.py @@ -2,11 +2,10 @@ def test_build_post_preview_embed_contains_platform_content(): - embed = build_post_preview_embed("Caching 101", "LinkedIn body") # Add `X body` this in the parameter when posting on X as well. + embed = build_post_preview_embed("Caching 101", "LinkedIn body with #hashtags") # Add `X body` this in the parameter when posting on X as well. assert embed.title == "New post idea: Caching 101" - assert embed.fields[0].name == "LinkedIn" - assert embed.fields[0].value == "LinkedIn body" + assert embed.description == "LinkedIn body with #hashtags" # Uncomment - If added credits on X platform. # assert embed.fields[1].name == "X" @@ -14,9 +13,9 @@ def test_build_post_preview_embed_contains_platform_content(): def test_build_post_preview_embed_truncates_long_content(): - long_text = "a" * 2000 + long_text = "a" * 5000 embed = build_post_preview_embed("Topic", long_text) # Add `long_text` this in the parameter when posting on X as well. - assert len(embed.fields[0].value) == 1024 - # assert len(embed.fields[1].value) == 1024 # Uncomment - If added credits on X platform. + assert len(embed.description) == 4096 + # assert len(embed.fields[1].value) == 4096 # Uncomment - If added credits on X platform. From 495a7819876ee06f27acab0e542ba3cfa36bc9ca Mon Sep 17 00:00:00 2001 From: kamlesh0928 Date: Sat, 26 Sep 2026 21:24:58 +0530 Subject: [PATCH 4/4] Fix the underlying gap of the plugin --- app/database/models/approval.py | 18 ++++++++++-------- app/database/models/post.py | 20 +++++++++++--------- app/database/models/publication.py | 20 +++++++++++--------- app/database/models/source.py | 20 +++++++++++--------- app/database/models/topic.py | 24 +++++++++++++----------- pyproject.toml | 2 +- 6 files changed, 57 insertions(+), 47 deletions(-) diff --git a/app/database/models/approval.py b/app/database/models/approval.py index be1f71c..4ad27ce 100644 --- a/app/database/models/approval.py +++ b/app/database/models/approval.py @@ -1,8 +1,9 @@ from __future__ import annotations -from sqlalchemy import Column, DateTime, ForeignKey, Integer, String -from sqlalchemy.orm import relationship -from sqlalchemy.sql import func +from datetime import datetime + +from sqlalchemy import DateTime, ForeignKey, Integer, String, func +from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database.base import Base @@ -10,10 +11,11 @@ class Approval(Base): __tablename__ = "approvals" - id = Column(Integer, primary_key=True, index=True) - post_id = Column(Integer, ForeignKey("posts.id"), nullable=False) - approver_name = Column(String(255), nullable=False) - approval_status = Column(String(50), nullable=False) - created_at = Column(DateTime(timezone=True), server_default=func.now()) + id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) + post_id: Mapped[int] = mapped_column(Integer, ForeignKey("posts.id"), nullable=False) + approver_name: Mapped[str] = mapped_column(String(255), nullable=False) + approval_status: Mapped[str] = mapped_column(String(50), nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) post = relationship("Post", back_populates="approvals", uselist=False) # type: ignore + \ No newline at end of file diff --git a/app/database/models/post.py b/app/database/models/post.py index 16153d9..af3ab55 100644 --- a/app/database/models/post.py +++ b/app/database/models/post.py @@ -1,8 +1,9 @@ from __future__ import annotations -from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text -from sqlalchemy.orm import relationship -from sqlalchemy.sql import func +from datetime import datetime + +from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database.base import Base @@ -10,13 +11,14 @@ class Post(Base): __tablename__ = "posts" - id = Column(Integer, primary_key=True, index=True) - title = Column(String(255), nullable=False) - content = Column(Text, nullable=False) - topic_id = Column(Integer, ForeignKey("topics.id"), nullable=False) - created_at = Column(DateTime(timezone=True), server_default=func.now()) + id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) + title: Mapped[str] = mapped_column(String(255), nullable=False) + content: Mapped[str] = mapped_column(Text, nullable=False) + topic_id: Mapped[int] = mapped_column(Integer, ForeignKey("topics.id"), nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) topic = relationship("Topic", back_populates="posts", uselist=False) # type: ignore approvals = relationship("Approval", back_populates="post", uselist=True) # type: ignore publications = relationship("Publication", back_populates="post", uselist=True) # type: ignore - sources = relationship("Source", back_populates="post", uselist=True) # type: ignore \ No newline at end of file + sources = relationship("Source", back_populates="post", uselist=True) # type: ignore + \ No newline at end of file diff --git a/app/database/models/publication.py b/app/database/models/publication.py index 6fa58e3..06730ee 100644 --- a/app/database/models/publication.py +++ b/app/database/models/publication.py @@ -1,8 +1,9 @@ from __future__ import annotations -from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text -from sqlalchemy.orm import relationship -from sqlalchemy.sql import func +from datetime import datetime + +from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database.base import Base @@ -10,10 +11,11 @@ class Publication(Base): __tablename__ = "publications" - id = Column(Integer, primary_key=True, index=True) - title = Column(String(255), nullable=False) - content = Column(Text, nullable=False) - post_id = Column(Integer, ForeignKey("posts.id"), nullable=False) - created_at = Column(DateTime(timezone=True), server_default=func.now()) + id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) + title: Mapped[str] = mapped_column(String(255), nullable=False) + content: Mapped[str] = mapped_column(Text, nullable=False) + post_id: Mapped[int] = mapped_column(Integer, ForeignKey("posts.id"), nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) - post = relationship("Post", back_populates="publications", uselist=False) # type: ignore \ No newline at end of file + post = relationship("Post", back_populates="publications", uselist=False) # type: ignore + \ No newline at end of file diff --git a/app/database/models/source.py b/app/database/models/source.py index 9b82519..b06c74a 100644 --- a/app/database/models/source.py +++ b/app/database/models/source.py @@ -1,8 +1,9 @@ from __future__ import annotations -from sqlalchemy import Column, DateTime, ForeignKey, Integer, String -from sqlalchemy.orm import relationship -from sqlalchemy.sql import func +from datetime import datetime + +from sqlalchemy import DateTime, ForeignKey, Integer, String, func +from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database.base import Base @@ -10,10 +11,11 @@ class Source(Base): __tablename__ = "sources" - id = Column(Integer, primary_key=True, index=True) - name = Column(String(255), nullable=False) - url = Column(String(255), nullable=False) - post_id = Column(Integer, ForeignKey("posts.id"), nullable=True) - created_at = Column(DateTime(timezone=True), server_default=func.now()) + id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) + name: Mapped[str] = mapped_column(String(255), nullable=False) + url: Mapped[str] = mapped_column(String(255), nullable=False) + post_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("posts.id"), nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) - post = relationship("Post", back_populates="sources", uselist=False) # type: ignore \ No newline at end of file + post = relationship("Post", back_populates="sources", uselist=False) # type: ignore + \ No newline at end of file diff --git a/app/database/models/topic.py b/app/database/models/topic.py index 9b1934b..17b57ce 100644 --- a/app/database/models/topic.py +++ b/app/database/models/topic.py @@ -1,8 +1,9 @@ from __future__ import annotations -from sqlalchemy import CheckConstraint, Column, DateTime, Integer, String, Text -from sqlalchemy.orm import relationship -from sqlalchemy.sql import func +from datetime import datetime + +from sqlalchemy import CheckConstraint, DateTime, Integer, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database.base import Base @@ -20,12 +21,13 @@ class Topic(Base): ), ) - id = Column(Integer, primary_key=True, index=True) - title = Column(String(255), nullable=False) - description = Column(Text, nullable=True) - rank = Column(Integer, nullable=False) - explanation = Column(Text, nullable=True) - status = Column(String(20), nullable=False, default=STATUS_PENDING) - created_at = Column(DateTime(timezone=True), server_default=func.now()) + id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) + title: Mapped[str] = mapped_column(String(255), nullable=False) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + rank: Mapped[int] = mapped_column(Integer, nullable=False) + explanation: Mapped[str | None] = mapped_column(Text, nullable=True) + status: Mapped[str] = mapped_column(String(20), nullable=False, default=STATUS_PENDING) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) - posts = relationship("Post", back_populates="topic", uselist=True) # type: ignore \ No newline at end of file + posts = relationship("Post", back_populates="topic", uselist=True) # type: ignore + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 0c3d161..2af897d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,2 @@ [tool.mypy] -plugins = ["pydantic.mypy", "sqlalchemy.ext.mypy.plugin"] \ No newline at end of file +plugins = ["pydantic.mypy"] \ No newline at end of file