-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathiterative_tools.py
More file actions
215 lines (167 loc) · 6.03 KB
/
Copy pathiterative_tools.py
File metadata and controls
215 lines (167 loc) · 6.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
import os
from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import ToolNode
from langchain_groq import ChatGroq
from langchain_openai import ChatOpenAI
from langchain_tavily import TavilySearch
from dotenv import load_dotenv
load_dotenv()
#tools
search_tool = TavilySearch(max_results = 3)
tools = [search_tool]
#llms
#writer
writer_llm = ChatOpenAI(model= "gpt-4o-mini",temperature=0.7)
writer_llm_with_tools = writer_llm.bind_tools(tools)
#reviewer
reviewer_llm = ChatGroq(model="llama-3.3-70b-versatile", temperature=0.2)
#state building
class State(TypedDict):
topic : str
messages : Annotated[list,add_messages]
draft : str
review_feedback : str
is_approved : bool
attempt : int
#nodes
WRITER_SYSTEM_PROMPT = (
"You are an expert LinkedIn content writer. Your job is to write "
"engaging, professional LinkedIn posts about the given topic. "
"If the topic requires up-to-date information, statistics, or "
"current trends, use the web search tool to gather fresh context "
"before writing. If you have already received feedback on a "
"previous draft, carefully address every point in the new draft. "
"Rules for good LinkedIn posts: strong hook in the first line, "
"1 clear takeaway, easy to skim (short paragraphs), around "
"150–200 words, ends with a question or call-to-action to invite "
"engagement. Do not use hashtags."
)
def writer_node(state : State) -> dict:
"""Writes (or rewrites) the LinkedIn post. Can call Tavily to search first."""
attempt = state.get("attempt",0) + 1
topic = state["topic"]
previous_feedback = state['review_feedback']
if attempt == 1:
user_message = (
f"Write a LinkedIn post on this topic {topic}"
f"if you need current info search the web first "
)
else:
user_message = (
f"your previous draft on '{topic}' was rejected"
f"Here is the reviewer's feedback \n\n {previous_feedback}\n\n"
f"Write a new, improved draft that fixes every issue mentiond"
f"do not repeat the same mistake"
)
messages = [("system",WRITER_SYSTEM_PROMPT),("human",user_message)]
response = writer_llm_with_tools.invoke(messages)
return {
"messages" : [("human",user_message),response],
"attempt" : attempt
}
tool_node = ToolNode(tools)
def extract_draft_node(state:State) -> dict:
"""After the writer finishes tool calls, pulls the final text out as the draft."""
last_message = state['messages'][-1]
draft = last_message.content
print(f"\n\n generated post \n {draft} \n ")
return {"draft" : draft}
REVIEWER_SYSTEM_PROMPT = (
"You are a strict LinkedIn content reviewer. You judge whether a "
"post is publish-ready. Evaluate against these criteria:\n"
"1. Strong hook in the first line\n"
"2. One clear, valuable takeaway\n"
"3. Easy to skim — uses short paragraphs\n"
"4. Roughly 150-200 words\n"
"5. Ends with an engaging question or CTA\n"
"6. Professional but human tone (not corporate-robotic)\n"
"7. No hashtags\n\n"
"Respond in exactly this format:\n"
"VERDICT: APPROVED or REJECTED\n"
"FEEDBACK: <one short paragraph explaining why>\n\n"
"Be strict but fair. Approve only if the post genuinely meets all "
"criteria. Reject if even one criterion is clearly missing."
)
def reviewer_node(state:State) -> dict:
"""Reviews the draft and decides: approve or reject with feedback."""
draft = state['draft']
prompt = (
f"review this LinkedIn post draft : \n"
f"{draft}\n"
f"give your reviews"
)
response = reviewer_llm.invoke(
[("system",REVIEWER_SYSTEM_PROMPT),("human",prompt)]
)
review_text = response.content.strip()
is_approved = "APPROVED" in review_text.upper().split("FEEDBACK")[0]
if "FEEDBACK:" in review_text:
feedback = review_text.split("FEEDBACK:", 1)[1].strip()
else:
feedback = review_text
verdict = "APPROVED" if is_approved else "REJECTED"
print(f"[Verdict: {verdict}]")
print(f"[Feedback: {feedback}]")
return {
"review_feedback": feedback,
"is_approved": is_approved,
}
#router function
def should_use_tool(state:State):
last_message = state['messages'][-1]
if getattr(last_message,'tool_calls',None):
return "tools"
return "extract_draft"
def should_stop_looping(state:State):
if state['is_approved']:
print("post haas been approved \n")
return END
if state['attempt'] >= 3:
print("reached max attempts")
return END
return "writer"
#build the graph
graph = StateGraph(State)
graph.add_node("writer",writer_node)
graph.add_node("tools",tool_node)
graph.add_node("extract_draft",extract_draft_node)
graph.add_node("reviewer",reviewer_node)
graph.add_edge(START,"writer")
graph.add_conditional_edges(
"writer",should_use_tool,
)
graph.add_edge("tools","reviewer")
graph.add_edge("extract_draft", "reviewer")
graph.add_conditional_edges(
"reviewer",should_stop_looping
)
app = graph.compile()
print("=" * 55)
print("Welcome to the LinkedIn Post Generator")
print("=" * 55)
print("\nThis tool will draft a LinkedIn post for you, review it")
print("itself, and iterate until it's publish-ready.")
print("=" * 55)
topic = input("\nWhat topic do you want a LinkedIn post about?\n> ").strip()
if not topic:
print("\nNo topic given. Exiting.")
else:
print("\nStarting generation...\n")
initial_state = {
"topic": topic,
"messages": [],
"draft": "",
"review_feedback": "",
"is_approved": False,
"attempt": 0,
}
final_state = app.invoke(initial_state)
print("\n" + "=" * 55)
print("FINAL LINKEDIN POST")
print("=" * 55)
print(final_state["draft"])
print("=" * 55)
print(f"Total attempts: {final_state['attempt']}")
print(f"Approved: {final_state['is_approved']}")