-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathgraphlink_note_agent.py
More file actions
270 lines (217 loc) · 10.8 KB
/
Copy pathgraphlink_note_agent.py
File metadata and controls
270 lines (217 loc) · 10.8 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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
"""Qt-free note-agent core: Key Takeaway and Explainer Note (R8a).
Both agents were implemented in the Qt app's graphlink_agents_core.py and
were lost to the R7.6b cutover, leaving their two chat-node menu items
rendered as disabled stubs whose tooltip blamed a missing agent layer. That
blocker had in fact been gone since R4 - the same dispatcher that already
drives Regenerate Response, Generate Image and Generate Chart. Nothing was
blocked; the two agents had simply never been ported. This module restores
them.
The prompts, the `clean_agent_markdown_response` post-processor and the
"one chat call, no tools, no history" shape are carried over verbatim from
the deleted implementation, so output matches what the Qt app produced.
Two deliberate divergences from that original, both because the machinery
it depended on no longer exists:
- No text bounding. Legacy passed source text through
`render_context(source_snapshot(...))`, which died with the Qt app and
has no successor. Every surviving agent path (chart, image, chat) feeds
unbounded text today, so this matches them rather than reintroducing a
single bounded path. If context limits ever need enforcing, that is a
systemic concern for all agents, not one this feature should solve
alone.
- No QThread workers. The dispatcher owns concurrency now
(backend/agents.py's start_note_generation), so the legacy
`KeyTakeawayWorkerThread`/`ExplainerWorkerThread` classes have no
counterpart and are not ported.
This file must stay Qt-free forever - it exists to be importable from
backend/, which test_no_qt_anywhere.py holds to zero tolerance.
"""
import api_provider
# graphlink_task_config (NOT graphlink_config) - the R4.1 Qt-free split of
# task/provider/model config. graphlink_config chains to PySide6.QtGui, so
# importing it here would silently re-taint this module with Qt.
import graphlink_task_config as config
def clean_agent_markdown_response(
text,
required_title,
section_markers,
reset_bullet_state_on_section_header=False,
):
"""Strip common markdown noise and normalize bullets/section spacing for a
structured agent response.
Ported verbatim from the deleted graphlink_agents_core.py, including the
`reset_bullet_state_on_section_header` flag: the legacy GroupSummaryAgent
set it and the two agents here did not, and that difference is preserved
rather than normalized away so their output is unchanged. (Group Summary
itself is not ported - its menu entry was conditional on a multi-select
model the new stack does not have.)
Args:
text (str): The raw text from the AI model.
required_title (str): Header line to prepend if the first cleaned line
doesn't already contain it.
section_markers (list[str]): Line substrings (e.g. "Key Parts:") that
get an extra blank line before them.
reset_bullet_state_on_section_header (bool): Whether a section-marker
line resets bullet-run tracking.
Returns:
str: The cleaned and formatted text.
"""
replacements = [
('```', ''),
('`', ''),
('**', ''),
('__', ''),
('*', ''),
('_', ''),
('•', '•'),
('→', '->'),
('\n\n\n', '\n\n'),
]
cleaned = str(text or "")
for old, new in replacements:
cleaned = cleaned.replace(old, new)
cleaned_lines = []
for line in cleaned.split('\n'):
line = line.strip()
if line:
if line.lstrip().startswith('-'):
line = '• ' + line.lstrip('- ')
cleaned_lines.append(line)
formatted = ''
in_bullet_list = False
for i, line in enumerate(cleaned_lines):
if i == 0 and required_title not in line:
formatted += f"{required_title}\n"
if line.startswith('•'):
if not in_bullet_list:
formatted += '\n' if formatted else ''
in_bullet_list = True
formatted += line + '\n'
elif any(marker in line for marker in section_markers):
formatted += '\n' + line + '\n'
if reset_bullet_state_on_section_header:
in_bullet_list = False
else:
in_bullet_list = False
formatted += line + '\n'
return formatted.strip()
class KeyTakeawayAgent:
"""Extracts key takeaways from a block of text."""
def __init__(self):
self.system_prompt = """You are a key takeaway generator. Format your response exactly like this:
Key Takeaway
[1-2 sentence overview]
Main Points:
• [First key point]
• [Second key point]
• [Third key point if needed]
Keep total output under 150 words. Be direct and focused on practical value.
No markdown formatting, no special characters."""
def clean_text(self, text):
return clean_agent_markdown_response(
text,
required_title="Key Takeaway",
section_markers=['Main Points:'],
)
def get_response(self, text):
messages = [
{'role': 'system', 'content': self.system_prompt},
{'role': 'user', 'content': f"Generate key takeaways from this text: {text}"},
]
response = api_provider.chat(task=config.TASK_CHAT, messages=messages)
return self.clean_text(response['message']['content'])
class BranchComparisonAgent:
"""ADR-002 Workstream 1 ("Compare Branches"): compares 2+ independent
conversation branches that diverged from the same starting point.
Genuinely new (not ported from anywhere) - GraphLink's branching
workflow never had a compare feature at all until this one. Same
"one chat call, no tools, no history" shape as KeyTakeawayAgent/
ExplainerAgent above, and reuses their shared clean_agent_markdown_
response post-processor rather than inventing a new formatter."""
def __init__(self):
self.system_prompt = """You are a branch-comparison analyst. You will be given two or more independent conversation branches that were explored from the same starting point. Compare them and format your response exactly like this:
Branch Comparison
Agreements:
• [a point where the branches agree, or "None found" if there are none]
Differences:
• [a point where the branches diverge - name which branch says what]
Unique Findings:
• [something only one branch surfaced - name which branch]
Unresolved Questions:
• [an open question none of the branches resolved]
Reference specific claims from the branches, not generic observations. Keep it focused and concrete. No markdown formatting, no special characters."""
def clean_text(self, text):
return clean_agent_markdown_response(
text,
required_title="Branch Comparison",
section_markers=["Agreements:", "Differences:", "Unique Findings:", "Unresolved Questions:"],
)
def get_response(self, formatted_branches_text):
messages = [
{'role': 'system', 'content': self.system_prompt},
{'role': 'user', 'content': f"Compare these branches:\n\n{formatted_branches_text}"},
]
response = api_provider.chat(task=config.TASK_CHAT, messages=messages)
return self.clean_text(response['message']['content'])
class BranchSynthesisAgent:
"""ADR-002 Workstream 1 ("Synthesize Branches"): combines 2+ independent
conversation branches into one answer, steered by the user's own
free-text instructions (e.g. "merge the best parts" or "pick whichever
branch's approach is simpler and explain why"). Deliberately NOT run
through clean_agent_markdown_response like the note agents above: its
result becomes a real chat-kind node rendered through the same Markdown
pipeline as an ordinary assistant reply (see SceneDocument.synthesize_
branches's own docstring), not a plain-text note - stripping markdown
from it would be actively wrong, not just unnecessary. No fixed
section-header format either, unlike the note agents: the whole point is
that the user's instructions shape the output, so a required template
would fight them."""
def __init__(self):
self.system_prompt = """You are a branch-synthesis assistant. You will be given two or more independent conversation branches that were explored from the same starting point, plus instructions from the user describing how to combine them. Follow the user's instructions to produce a single, coherent answer that draws on the branches as source material. Reference specific content from the branches rather than generic observations. You may use normal Markdown formatting (headings, bold, code blocks, lists) since your answer will be rendered as a real chat message, not a plain-text note."""
def get_response(self, formatted_branches_text, instructions):
messages = [
{'role': 'system', 'content': self.system_prompt},
{
'role': 'user',
'content': (
f"Instructions: {instructions}\n\n"
f"Branches:\n\n{formatted_branches_text}"
),
},
]
response = api_provider.chat(task=config.TASK_CHAT, messages=messages)
return str(response['message']['content'] or "").strip()
class ExplainerAgent:
"""Simplifies complex topics into plain language."""
def __init__(self):
self.system_prompt = """You are an expert at explaining complex topics in simple terms. Follow these principles in order:
1. Simplification: Break down complex ideas into their most basic form
2. Clarification: Remove any technical jargon or complex terminology
3. Distillation: Extract only the most important concepts
4. Breakdown: Present information in small, digestible chunks
5. Simple Language: Use everyday words and short sentences
Always use:
- Analogies: Connect ideas to everyday experiences
- Metaphors: Compare complex concepts to simple, familiar things
Format your response exactly like this:
Simple Explanation
[2-3 sentence overview using everyday language]
Think of it Like This:
[Add one clear analogy or metaphor that a child would understand]
Key Parts:
• [First simple point]
• [Second simple point]
• [Third point if needed]
Remember: Write as if explaining to a curious 5-year-old. No technical terms, no complex words."""
def clean_text(self, text):
return clean_agent_markdown_response(
text,
required_title="Simple Explanation",
section_markers=['Think of it Like This:', 'Key Parts:'],
)
def get_response(self, text):
messages = [
{'role': 'system', 'content': self.system_prompt},
{'role': 'user', 'content': f"Explain this in simple terms: {text}"},
]
response = api_provider.chat(task=config.TASK_CHAT, messages=messages)
return self.clean_text(response['message']['content'])