hyperforge-summarize 1.0.0.post21__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ from .agent import SummarizeAgent
2
+
3
+ __all__ = [
4
+ "SummarizeAgent",
5
+ ]
@@ -0,0 +1,377 @@
1
+ from time import time
2
+ from typing import List, Optional, overload
3
+
4
+ from hyperforge import PROMPT_ENVIRONMENT
5
+ from hyperforge.agent import Agent
6
+ from hyperforge.configure import agent
7
+ from hyperforge.context.agent import generate_ctx_block_id
8
+ from hyperforge.manager import Manager
9
+ from hyperforge.memory import QuestionMemory
10
+ from hyperforge.models import AnswerCitations, CitationMetadata, Context
11
+ from hyperforge.trace import trace_agent
12
+ from nuclia.lib.nua_responses import ChatModel, Tool, ToolChoiceAuto, UserPrompt
13
+ from nuclia_models.predict.generative_responses import ToolCall
14
+
15
+ from hyperforge_summarize.config import SummarizeAgentConfig
16
+ from hyperforge_summarize.prompts import MARKDOWN_TWO_LEVELS_CITATIONS_PROMPT_ADJUSTMENT
17
+
18
+ DEFAULT_SYSTEM_PROMPT = """You are a helpful AI assistant. Your role is to provide accurate, clear, and well-structured answers based strictly on the information provided to you.
19
+ Key principles:
20
+ - Answer only using the information in the provided context
21
+ - Do not use external knowledge, assumptions, or prior experience
22
+ - Maintain a professional and informative tone
23
+ - Be concise yet thorough
24
+ - If information is insufficient, acknowledge this clearly
25
+
26
+ Always follow any additional instructions provided about format, style, or domain-specific behavior."""
27
+
28
+ SUMMARIZE_PROMPT_CONVERSATIONAL = """
29
+ {% if rules -%}
30
+ # Generation Rules
31
+ {% for rule in rules -%}
32
+ - {{ rule }}
33
+ {% endfor -%}
34
+ {% endif -%}
35
+
36
+
37
+ ## Question
38
+ {{ question }}
39
+
40
+ ## Provided Context
41
+ [START OF CONTEXT]
42
+ {{ context }}
43
+ [END OF CONTEXT]
44
+
45
+ ## Answering Guidelines
46
+ - Carefully read all context; it may be lengthy or detailed
47
+ - Do not omit or overlook any relevant information
48
+ - If the context is incomplete or insufficient, try to provide a partial answer and encourage the user to clarify their question
49
+ - Read carefully any extra instructions below if provided and use them to answer
50
+
51
+ {% if prompt -%}
52
+ ## Additional Instructions for answering
53
+ {{ prompt }}
54
+ {% endif -%}
55
+
56
+ {% if chat_history -%}
57
+ ## Previous conversation history
58
+ - {{ chat_history }}
59
+ {% endif -%}
60
+
61
+ {% if extra_prompts -%}
62
+ ## Extra Prompts to consider for generating the answer
63
+ This information was used to generate the context.
64
+ Use it to help generate a better answer and follow any specific instructions it may contain about the format or style of the answer.
65
+ {% for extra in extra_prompts -%}
66
+ - {{ extra }}
67
+ {% endfor -%}
68
+ {% endif -%}
69
+
70
+ Now provide your answer to the question: {{ question }}
71
+ """
72
+
73
+ SUMMARIZE_PROMPT = """
74
+ {% if rules -%}
75
+ # Generation Rules
76
+ {% for rule in rules -%}
77
+ - {{ rule }}
78
+ {% endfor -%}
79
+ {% endif -%}
80
+
81
+ ## Question
82
+ {{ question }}
83
+
84
+ ## Provided Context
85
+ [START OF CONTEXT]
86
+ {{ context }}
87
+ [END OF CONTEXT]
88
+
89
+ ## Answering Guidelines
90
+ - Carefully read all context; it may be lengthy or detailed
91
+ - Do not omit or overlook any relevant information
92
+ - If the context is incomplete or insufficient, state: "Not enough data to answer this."
93
+ - Read carefully any extra instructions below if provided and use them to answer
94
+
95
+ {% if prompt -%}
96
+ ## Additional Instructions for answering
97
+ - {{ prompt }}
98
+ {% endif -%}
99
+
100
+ {% if chat_history -%}
101
+ ## Previous conversation history
102
+ - {{ chat_history }}
103
+ {% endif -%}
104
+
105
+
106
+ {% if extra_prompts -%}
107
+ ## Extra Prompts to consider for generating the answer
108
+ This information was used to generate the context.
109
+ Use it to help generate a better answer and follow any specific instructions it may contain about the format or style of the answer.
110
+ {% for extra in extra_prompts -%}
111
+ - {{ extra }}
112
+ {% endfor -%}
113
+ {% endif -%}
114
+
115
+ Now provide your answer to the question: {{ question }}
116
+ """
117
+
118
+ SUMMARIZE_PROMPT_TEMPLATE = PROMPT_ENVIRONMENT.from_string(SUMMARIZE_PROMPT)
119
+ SUMMARIZE_PROMPT_CONVERSATIONAL_TEMPLATE = PROMPT_ENVIRONMENT.from_string(
120
+ SUMMARIZE_PROMPT_CONVERSATIONAL
121
+ )
122
+
123
+
124
+ @agent(
125
+ id="summarize",
126
+ agent_type="generation",
127
+ title="Summarize",
128
+ description="Summarize the provided context.",
129
+ config_schema=SummarizeAgentConfig,
130
+ )
131
+ class SummarizeAgent(Agent[SummarizeAgentConfig]):
132
+ __root_agent__ = True
133
+ config: SummarizeAgentConfig
134
+
135
+ @overload
136
+ async def __call__(
137
+ self,
138
+ memory: QuestionMemory,
139
+ manager: Manager,
140
+ tools: None = None,
141
+ ) -> None: ...
142
+
143
+ @overload
144
+ async def __call__(
145
+ self,
146
+ memory: QuestionMemory,
147
+ manager: Manager,
148
+ tools: list[Tool],
149
+ ) -> None | dict[str, list[ToolCall]]: ...
150
+
151
+ @trace_agent
152
+ async def __call__(
153
+ self,
154
+ memory: QuestionMemory,
155
+ manager: Manager,
156
+ tools: list[Tool] | None = None,
157
+ ) -> None | dict[str, list[ToolCall]]:
158
+ citations_enabled = self.config.citations
159
+
160
+ # For each context in memory add context query, summary and answer onto a text and the initial question
161
+ # In cases when the original question has been rephrased, use the rephrased question
162
+ questions = memory.get_questions()
163
+ if len(questions) == 1:
164
+ question = questions[0][1]
165
+ else:
166
+ question = memory.original_question
167
+
168
+ session_context_parts: List[str] = []
169
+
170
+ if self.config.history:
171
+ qa_history, interactions = await memory.context_history()
172
+ await memory.add_step(
173
+ step_module=self.config.module,
174
+ step_title=self.step_title("History check"),
175
+ step_value="Included {} interactions of Q&A history".format(
176
+ interactions
177
+ ),
178
+ step_reason="",
179
+ timeit=0,
180
+ step_agent_path=f"/context/{self.config.id if self.config.id else 'default'}",
181
+ input_nuclia_tokens=0.0,
182
+ output_nuclia_tokens=0.0,
183
+ )
184
+ session_context_parts.append(
185
+ f"## Previous questions and answers in this session:\n{qa_history}"
186
+ )
187
+
188
+ session_context = "\n\n".join(session_context_parts)
189
+
190
+ PROMPT_TEMPLATE = (
191
+ SUMMARIZE_PROMPT_CONVERSATIONAL_TEMPLATE
192
+ if self.config.conversational
193
+ else SUMMARIZE_PROMPT_TEMPLATE
194
+ )
195
+ prompt = self.config.prompt
196
+ extra_prompts: List[str] = []
197
+ if self.config.include_mcp_prompts:
198
+ extra_prompts = memory.get_prompt_texts()
199
+
200
+ if citations_enabled:
201
+ # Add citation ids to each context so they can be referenced in the answer
202
+ for index, context in enumerate(memory.contexts):
203
+ context.citations_id = generate_ctx_block_id(index)
204
+
205
+ prompt = PROMPT_TEMPLATE.render(
206
+ question=question,
207
+ context=memory.contexts_markdown()
208
+ if citations_enabled and self.config.force_chunk_level_citations
209
+ else memory.contexts_minimal(),
210
+ prompt=prompt,
211
+ extra_prompts=extra_prompts,
212
+ rules=self.config.rules,
213
+ chat_history=session_context,
214
+ )
215
+
216
+ if citations_enabled:
217
+ # Adjust the prompt so that the model returns citations
218
+ prompt += MARKDOWN_TWO_LEVELS_CITATIONS_PROMPT_ADJUSTMENT
219
+
220
+ t0 = time()
221
+ images = {}
222
+ for memory_context in memory.contexts:
223
+ if memory_context.images:
224
+ images.update(memory_context.images)
225
+ chat_model = ChatModel(
226
+ user_id="summarize",
227
+ question="",
228
+ user_prompt=UserPrompt(prompt=prompt),
229
+ system=self.config.system_prompt
230
+ if self.config.system_prompt
231
+ else DEFAULT_SYSTEM_PROMPT,
232
+ format_prompt=False,
233
+ generative_model=self.config.model,
234
+ query_context_images=images,
235
+ max_tokens=5000,
236
+ chat_history=await memory.get_chat_history(),
237
+ tools=tools if tools else [],
238
+ tool_choice=ToolChoiceAuto(),
239
+ )
240
+
241
+ agent_path = f"/generation/{self.config.id if self.config.id else 'default'}"
242
+ # Pass memory so execute_raw streams automatically when memory.streaming is True.
243
+ # Streaming is skipped when tools are active (tool calls are not streamable).
244
+ streaming_memory = memory if not tools else None
245
+ resp, input_tokens, output_tokens = await manager.execute_raw(
246
+ chat_model,
247
+ memory=streaming_memory,
248
+ module="summarize",
249
+ agent_path=agent_path,
250
+ tracking=memory.get_tracking_info(),
251
+ )
252
+ answer = resp.answer
253
+ end_code = resp.code
254
+
255
+ # Fall back to original question and full contexts
256
+ if end_code == "-2": # indicates not enough data
257
+ prompt = PROMPT_TEMPLATE.render(
258
+ question=memory.original_question,
259
+ context=memory.contexts_markdown(),
260
+ prompt=prompt,
261
+ extra_prompts=extra_prompts,
262
+ rules=self.config.rules,
263
+ )
264
+ if citations_enabled:
265
+ # Adjust the prompt so that the model returns citations
266
+ prompt += MARKDOWN_TWO_LEVELS_CITATIONS_PROMPT_ADJUSTMENT
267
+
268
+ chat_model.user_prompt = UserPrompt(prompt=prompt)
269
+ resp, input_tokens, output_tokens = await manager.execute_raw(
270
+ chat_model,
271
+ memory=streaming_memory,
272
+ module="summarize",
273
+ agent_path=agent_path,
274
+ tracking=memory.get_tracking_info(),
275
+ )
276
+ answer = resp.answer
277
+ end_code = resp.code
278
+
279
+ if not resp.tools or answer:
280
+ # Only add answer if not a tool call or if answer is present (maybe some models return both)
281
+ await memory.add_answer(
282
+ answer,
283
+ module="summarize",
284
+ agent_path=f"/generation/{self.config.id if self.config.id else 'default'}",
285
+ citations=build_answer_citations(answer, memory.contexts)
286
+ if citations_enabled
287
+ else None,
288
+ )
289
+ memory.is_answered = end_code != "-2"
290
+ await memory.add_step(
291
+ step_module=self.config.module,
292
+ step_title=self.step_title("Summarize"),
293
+ step_value=str(answer),
294
+ step_reason="Summarized",
295
+ step_agent_path=f"/generation/{self.config.id if self.config.id else 'default'}",
296
+ timeit=time() - t0,
297
+ input_nuclia_tokens=input_tokens,
298
+ output_nuclia_tokens=output_tokens,
299
+ )
300
+ return resp.tools
301
+
302
+
303
+ def build_answer_citations(answer: str, contexts: list[Context]) -> AnswerCitations:
304
+ result = AnswerCitations()
305
+ # Build a map of citation_id to context
306
+ citation_map = {
307
+ context.citations_id: context
308
+ for context in contexts
309
+ if context.citations_id is not None
310
+ }
311
+
312
+ # Parse citations in the answer
313
+ citations_in_answer: set[str] = set()
314
+ for line in answer.splitlines():
315
+ if line.startswith("[") and "]: block-" in line:
316
+ citation_id = line.split("]: ")[1].strip()
317
+ citations_in_answer.add(citation_id)
318
+
319
+ for citation_id in citations_in_answer:
320
+ try:
321
+ context_citation_id, chunk_index = _parse_citation_id(citation_id)
322
+ except ValueError:
323
+ # Unknown format, skip
324
+ continue
325
+
326
+ if context_citation_id not in citation_map:
327
+ # Unknown citation, skip
328
+ continue
329
+
330
+ context: Context = citation_map[context_citation_id]
331
+ origin_urls: list[str] = []
332
+
333
+ if chunk_index is not None:
334
+ # This is a citation to a specific chunk of a context
335
+ try:
336
+ chunk = context.chunks[chunk_index]
337
+ if chunk.origin_url:
338
+ origin_urls.append(chunk.origin_url)
339
+ except IndexError:
340
+ # Chunk index is out of range, skip
341
+ pass
342
+
343
+ else:
344
+ # This is a citation to a summarized context
345
+ for chunk in context.chunks:
346
+ if (
347
+ context.citations is None or chunk.chunk_id in context.citations
348
+ ) and chunk.origin_url:
349
+ origin_urls.append(chunk.origin_url)
350
+
351
+ result.metadata[citation_id] = CitationMetadata(
352
+ context_id=context.id,
353
+ origin_urls=origin_urls,
354
+ chunk_index=chunk_index,
355
+ )
356
+
357
+ return result
358
+
359
+
360
+ def _parse_citation_id(citation_id: str) -> tuple[str, Optional[int]]:
361
+ """Parse a citation id into context citation id and optional chunk index.
362
+
363
+ Examples:
364
+ - "block-abc123" -> ("block-abc123", None)
365
+ - "block-abc123-0" -> ("block-abc123", 0)
366
+ """
367
+ if citation_id.count("-") >= 2:
368
+ # Assume the last part is the chunk index
369
+ parts = citation_id.rsplit("-", 1)
370
+ context_citation_id = parts[0]
371
+ try:
372
+ chunk_index = int(parts[1])
373
+ return context_citation_id, chunk_index
374
+ except ValueError:
375
+ raise ValueError("Invalid citation id format")
376
+ else:
377
+ return citation_id, None
@@ -0,0 +1,52 @@
1
+ from typing import Literal, Optional
2
+
3
+ from hyperforge.agent import AgentConfig
4
+ from hyperforge.utils import WidgetType
5
+ from pydantic import Field
6
+ from pydantic.config import ConfigDict
7
+
8
+
9
+ class SummarizeAgentConfig(AgentConfig):
10
+ model_config = ConfigDict(title="Summarize")
11
+ module: Literal["summarize"] = "summarize"
12
+ system_prompt: Optional[str] = Field(
13
+ default=None,
14
+ title="System prompt",
15
+ description="System prompt to guide the model's behavior and response style",
16
+ json_schema_extra={
17
+ "show_in_node": True,
18
+ "widget": WidgetType.EXPANDABLE_TEXTAREA,
19
+ },
20
+ )
21
+ prompt: Optional[str] = Field(
22
+ default=None,
23
+ json_schema_extra={
24
+ "show_in_node": True,
25
+ "widget": WidgetType.EXPANDABLE_TEXTAREA,
26
+ },
27
+ )
28
+ model: str = Field(
29
+ default="chatgpt-azure-4o-mini",
30
+ title="Generative model",
31
+ description="Model used to generate the response",
32
+ json_schema_extra={"widget": WidgetType.MODEL_SELECT},
33
+ )
34
+ images: bool = False
35
+ conversational: bool = False
36
+ include_mcp_prompts: bool = Field(
37
+ default=False,
38
+ title="If MCP prompts were used during the context steps, include them in the prompt to generate the final answer",
39
+ )
40
+ citations: bool = Field(
41
+ default=False,
42
+ title="Whether to include markdown citations in the generated answer.",
43
+ )
44
+ force_chunk_level_citations: bool = Field(
45
+ default=False,
46
+ title="Whether to always use chunk-level citations instead of context-level citations.",
47
+ )
48
+ history: bool = Field(
49
+ default=False,
50
+ title="Session history",
51
+ description="Include previous Q&A history from the current session in the context provided to the summarize agent",
52
+ )
@@ -0,0 +1,39 @@
1
+ MARKDOWN_TWO_LEVELS_CITATIONS_PROMPT_ADJUSTMENT = """
2
+ You are given source blocks with IDs like: block-AB, block-BA, block-CD, etc. or block-AB-1, block-BA-2, block-CD-3, etc.
3
+ When producing an answer, cite these sources precisely using markdown footnotes.
4
+
5
+ CITATION RULES
6
+
7
+ 1. In the main body, cite sources only with bracketed Arabic numerals: [1], [2], [3], etc.
8
+ - Never put a block ID directly in brackets (e.g. NO: [AB], [block-AB], [BA]).
9
+ - Never mix styles (no superscripts, no inline block names, no [Ref 1], etc.).
10
+ 2. Numbering is assigned in order of FIRST USE of a unique block ID.
11
+ - The first time you need info from a block, assign it [1].
12
+ - The first time you need info from a never-before-used block, assign it the next unused number (e.g. [2]).
13
+ - If you later cite the SAME block again, REUSE its existing number (do NOT create a new one).
14
+ - This guarantees there are no duplicate footnote definitions and no gaps.
15
+ 3. If facts in a sentence come from different blocks, you may concatenate citations WITH spaces: like [1] [3]. Do NOT merge them (no ranges like [1-3]) or output them without spaces (no [1][3]).
16
+ 4. At the end, output section consisting ONLY of the unique citation mappings, one per line, in ascending numeric order
17
+ - Don't title this section or add any extra text, just the mappings.
18
+ - No duplicates.
19
+ - No skipped numbers.
20
+ - ONLY include blocks actually cited in the body.
21
+ 5. Do NOT hallucinate block IDs. Only use those provided in the context.
22
+
23
+ FORMATTING CONTRACT
24
+
25
+ * Body: free text with numeric citations as specified.
26
+ * A blank line.
27
+ * References section (if any) exactly as described, no heading.
28
+
29
+
30
+ Example format:
31
+
32
+ ---
33
+ "The OP-1 has a built-in tape feature with 6 minutes of recording time [1] [2]. You can record to any of the 4 individual tracks [1]."
34
+
35
+ [1]: block-AB
36
+ [2]: block-FZ-2
37
+ ---
38
+
39
+ """
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: hyperforge_summarize
3
+ Version: 1.0.0.post21
4
+ Summary: NucliaDB Hyperforge agent
5
+ Author-email: Nuclia <nucliadb@nuclia.com>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://progress.com
8
+ Project-URL: Repository, https://github.com/nuclia/forge
9
+ Classifier: Programming Language :: Python
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Requires-Python: <4,>=3.10
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: hyperforge
18
+
19
+ # Summarization Hyperforge agents
@@ -0,0 +1,8 @@
1
+ hyperforge_summarize/__init__.py,sha256=eqPf63ok3aXty9p_llAM9hfITEsCQ4s2h4iPPITrfQs,71
2
+ hyperforge_summarize/agent.py,sha256=2F7q-aWj5uk5GVcVr-wALpuXHUa0T7ZMe05iMW54Qxc,12987
3
+ hyperforge_summarize/config.py,sha256=hUt7SLarq3Qmpc5ryUMKCV1t68IAA6aeNzuPUAu63TA,1829
4
+ hyperforge_summarize/prompts.py,sha256=4Rz5WySdV_TPIxLPDMA7HljeePnVlCJK2YYoQ2u-OgU,1851
5
+ hyperforge_summarize-1.0.0.post21.dist-info/METADATA,sha256=fCwFVq-5E3kuD2vJb7E2f5kPscEomdc9UNJparB4vPM,734
6
+ hyperforge_summarize-1.0.0.post21.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ hyperforge_summarize-1.0.0.post21.dist-info/top_level.txt,sha256=8hkP4ie0zM_l2ulyGmPUCGDsjMyC2kM7en28kU2oUvg,21
8
+ hyperforge_summarize-1.0.0.post21.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ hyperforge_summarize