forked from balisujohn/localwriter
-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathprompts.py
More file actions
927 lines (726 loc) · 57.5 KB
/
Copy pathprompts.py
File metadata and controls
927 lines (726 loc) · 57.5 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
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
# WriterAgent - AI Writing Assistant for LibreOffice
# Copyright (c) 2024 John Balis
# Copyright (c) 2026 KeithCu (modifications and relicensing)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
# Prepended to the first string `system` message in LlmClient for non-release bundles only
# (``make build`` includes ``plugin/tests``; ``make release`` / ``--no-tests`` does not).
# See `should_prepend_dev_llm_system_prefix()`.
LLM_DEV_BUILD_SYSTEM_PREFIX = (
"[WriterAgent development build]\n"
"You are running a development version of the WriterAgent extension. The user is a plugin developer. "
"If you run into a problem, explain in detail what failed and why so they can improve the extension. "
"If they ask detailed questions about tool-calling APIs, prompts, or how the software works, answer helpfully so developers can improve the plugin."
)
def should_prepend_dev_llm_system_prefix() -> bool:
"""True when this bundle includes test modules (same signal as the optional Debug / in-OXT tests)."""
try:
import importlib.util
return importlib.util.find_spec("plugin.tests") is not None
except Exception:
return False
# ---------------------------------------------------------------------------
# Delegation primitives (inputs to core directives and tool schemas)
# ---------------------------------------------------------------------------
# Research routing (short); domain bullets use these strings as-is.
DELEGATION_USER_FILE_DATA_HINT = "to use information that is not in the current document, and may be in (my / our) personal or business documents"
DELEGATION_PUBLIC_WEB_HINT = "to research public topics"
# Main agent only: after research delegates return plain text, write HTML to the document (not sidebar).
RESEARCH_DELEGATE_TO_DOCUMENT = (
"After doing web_research or document_research, you MUST call apply_document_content to insert the received research into the document so the user can see and edit it (per APPLY_DOCUMENT_CONTENT rules). "
"Default: write the full report to the open document (empty doc → target='beginning'). "
"Sidebar: brief confirmation only — NEVER paste the full report in chat unless the user explicitly asked chat-only."
)
APPLY_DOCUMENT_CONTENT_TOOL_RESEARCH_HINT = "Required after web_research or document_research delegates return."
# Canonical wording for tools that return a para_index / paragraph_index to the model. Those indexes
# are internal addressing only; the user never sees them and they shift as the document changes, so
# the model must refer to a place by quoting its text, not by number. Append to such tool
# descriptions so the rule is stated uniformly wherever an index is exposed (#1).
PARAGRAPH_INDEX_DIRECTIVE = (
"para_index / paragraph_index values are INTERNAL addressing only — NEVER cite paragraph numbers "
"to the user (they don't see them and they shift as the document changes); to point the user at a "
"place, quote the first few words of its text instead (e.g. \"the sentence starting 'The Amazon…'\")."
)
def delegation_math_to_python_hint(*, delegate_toolset: str) -> str:
"""Writer/Draw: route computational math to the python specialized sub-agent (fast local venv)."""
return (
"For computational or numeric math (exact values, primes, statistics, symbolic algebra, or non-trivial calculation), "
f'do not answer from memory—use {delegate_toolset}(domain="python") for fast local numeric computation.'
)
# Brief hint for gateway tool JSON schemas (see SPECIALIZED_TASK_RULES in system prompt).
DELEGATE_SPECIALIZED_TASK_PARAM_HINT = "What the specialized task should accomplish."
# Shared guidance for writing `task` strings when delegating to specialized sub-agents.
SPECIALIZED_TASK_RULES = (
"Pass a clear `task` describing what the specialized task should accomplish."
)
# ---------------------------------------------------------------------------
# Shared HTML primitives (sidebar + document fragments)
# ---------------------------------------------------------------------------
# Tag-level rules shared by sidebar chat and apply_document_content fragments.
# Container differs: single HTML string (sidebar) vs JSON array (document) — docs/chat-sidebar-implementation.md § Chat prompt constants.
HTML_FRAGMENT_RULES = """
- Use <br> for line breaks within an element; <p> for paragraphs.
- Raw Unicode (é, ü, ©); straight double quotes ("), not curly/smart quotes or HTML entities. Send <h1> not <h1>. Preserve intentional spacing.
- Do NOT use Markdown (#, **, ```, etc.)."""
# Sidebar / sub-agent examples (single HTML string — not apply_document_content's array). See docs/chat-sidebar-implementation.md § Chat prompt constants.
CHAT_SIDEBAR_HTML_EXAMPLES = """
CHAT HTML EXAMPLES:
- Good: "<p>Paragraph with <strong>bold</strong> text.</p>"
- Bad: "**bold**" (Markdown)
- Bad: "<p>Paragraph</p>" (escaped entities)"""
# ---------------------------------------------------------------------------
# Writer chat system prompt (source order = runtime order)
# ---------------------------------------------------------------------------
WRITER_CHAT_PERSONA = """You are a LibreOffice Writer assistant who produces polished, professional documents with thoughtful use of color and formatting.
Honor any stated memory preferences for color, etc."""
CHAT_RESPONSE_FORMAT = """CHAT RESPONSE FORMAT: Format your conversational responses as HTML (use <p>, <strong>, <em>, <code>, <ul>, <ol>, <h2>, <pre>, <br>). The sidebar renders HTML natively."""
PLAIN_CHAT_RESPONSE_FORMAT = "CHAT RESPONSE FORMAT: Respond in plain text only. Do NOT use HTML tags or Markdown formatting (no #, **, ```, etc.)."
RICH_CHAT_SIDEBAR_INSTRUCTIONS = f"""{CHAT_RESPONSE_FORMAT}
{HTML_FRAGMENT_RULES}
{CHAT_SIDEBAR_HTML_EXAMPLES}"""
def get_chat_response_format_instructions(ctx=None) -> str:
"""Sidebar response format for main chat and sub-agents (web research, librarian).
When ``rich_text_control_sidebar`` is off, models are not told about HTML — same gate as
``get_chat_system_prompt_for_document``.
"""
from plugin.framework.config import get_config_bool_safe
if not get_config_bool_safe("rich_text_control_sidebar"):
return PLAIN_CHAT_RESPONSE_FORMAT
return RICH_CHAT_SIDEBAR_INSTRUCTIONS
# Main sidebar chat only (Writer DEFAULT_CHAT_SYSTEM_PROMPT_TEMPLATE). Sub-agents use
# final_answer / reply_to_user / delegate task — not this block.
SIDEBAR_VS_DOCUMENT = """SIDEBAR CHAT (main agent): Chat history is the sidebar only — not the document.
MUST use apply_document_content for drafts, reports, and research output; sidebar gets at most a brief confirmation.
Follow CHAT RESPONSE FORMAT for that short reply."""
# Writer main chat: delegation routing (paired with SIDEBAR_VS_DOCUMENT in the system prompt).
WRITER_CORE_DIRECTIVES = f"""When the user wants {DELEGATION_USER_FILE_DATA_HINT}:
- You MUST NOT ask the user where to find it, or to upload, paste, its contents.
- You MUST call delegate_to_specialized_writer_toolset(domain="document_research") once with their described file(s) and task in task; the specialized task lists nearby files to match (paths not required).
When the user wants {DELEGATION_PUBLIC_WEB_HINT}, delegate_to_specialized_writer_toolset(domain="web_research").
For web_research and document_research: describe what to research in `task` (topics, sections, depth). {RESEARCH_DELEGATE_TO_DOCUMENT}
{delegation_math_to_python_hint(delegate_toolset="delegate_to_specialized_writer_toolset")}
When asked to make a script or run Python, use delegate_to_specialized_writer_toolset(domain="python")."""
CALC_CORE_DIRECTIVES = f"""When the user wants {DELEGATION_USER_FILE_DATA_HINT} (including when the user refers to any other file, document, spreadsheet, or sheet by name or path, e.g. "my spreadsheet", "read cell a9 from PythonInCalc", "summary.odt", etc., or asks to pull, read, search, or reference data from them):
- You MUST NOT ask the user where the file is stored, how to find it, or to upload, paste, or share its contents.
- You MUST call delegate_to_specialized_calc_toolset(domain="document_research") once with their described file(s) and task in task; the specialized task lists nearby files to match (paths not required).
When the user wants {DELEGATION_PUBLIC_WEB_HINT}, delegate_to_specialized_calc_toolset(domain="web_research").
When the user wants data analysis, statistics, regression, clustering, forecasting or time series, charts or plots, Goal Seek, or Solver on spreadsheet data, delegate_to_specialized_calc_toolset(domain="analysis")."""
DRAW_CORE_DIRECTIVES = f"""When the user wants {DELEGATION_USER_FILE_DATA_HINT} (including when the user refers to any other file, document, spreadsheet, or sheet by name or path, e.g. "my spreadsheet", "read cell a9 from PythonInCalc", "summary.odt", etc., or asks to pull, read, search, or reference data from them):
- You MUST NOT ask the user where the file is stored, how to find it, or to upload, paste, or share its contents.
- You MUST call delegate_to_specialized_draw_toolset(domain="document_research") once with their described file(s) and task in task; the specialized task lists nearby files to match (paths not required).
When the user wants {DELEGATION_PUBLIC_WEB_HINT}, delegate_to_specialized_draw_toolset(domain="web_research").
{delegation_math_to_python_hint(delegate_toolset="delegate_to_specialized_draw_toolset")}
When asked to make a script or run Python, use delegate_to_specialized_draw_toolset(domain="python")."""
def get_core_directives(model) -> str:
"""Return the application-specific core directives dynamically based on document type."""
from plugin.doc.document_helpers import is_calc, is_draw
if is_calc(model):
return CALC_CORE_DIRECTIVES
elif is_draw(model):
return DRAW_CORE_DIRECTIVES
else:
return WRITER_CORE_DIRECTIVES
# ---------------------------------------------------------------------------
# Shared behavioral pieces (single source of truth for BOTH agents)
# ---------------------------------------------------------------------------
WRITER_REVIEW_MODES_RULES = """TRACKED CHANGES / REVIEW MODES:
- The user picks ONE of three review modes for your edits; you never pick or switch it.
- off: your edits apply directly and are live immediately.
- record: your edits ARE applied, but as tracked changes pending the user's accept/reject; you will NOT be told whether they are later accepted or rejected.
- wait: your edits are applied as tracked changes and the edit tool blocks until the user finishes reviewing (or a configured timeout), then the result reports what was accepted or rejected.
- apply_document_content's RESULT carries the review state for that call (record: review_mode / pending_review; wait: a review field with the outcome) — trust the latest result over anything earlier, since the user can switch modes mid-session.
- In record and wait, NEVER accept or reject changes yourself (no manage_tracked_changes accept/reject) — resolving redlines is the user's decision.
- When reading, get_document_content lists pending changes under tracked_changes (insertions/deletions) — they are pending review, not errors to fix."""
WRITER_SEARCH_RULES = """SEARCH:
- search_in_document finds text ANYWHERE — body paragraphs and headings, table cells, text boxes/frames, floating drawing shapes, page headers/footers, and comments.
- Each match reports WHERE it lives (e.g. "body", "table 'X' cell B2", "text box 'Y'", "shape 'Z'", "header (page style 'Standard')", "comment by 'A'") plus the surrounding text; use return_offsets=true for character ranges.
- When pointing the user to a match, quote the first words of its text and its location — never an internal paragraph index."""
WRITER_NAVIGATION_RULES = """NAVIGATING LARGE DOCUMENTS (map first, then drill — don't dump):
- get_document_tree(content_strategy='heading_only') gives the heading outline plus stats and stable _mcp_ bookmark ids.
- nav_heading_children (structural domain; locator='bookmark:_mcp_…' or 'heading:1.2') reads one section on demand.
- search_in_document jumps to specific text.
- Reserve get_document_content(scope='full') for short documents or a deliberate full read."""
WRITER_IMAGES_RULES = """IMAGES:
- Image tools live in the 'images' domain: image_insert, image_delete, image_replace, image_list, image_get_info (includes crop_mm), image_download. OCR (extract_text_from_image) lives in the 'vision' domain.
- image_set_properties resizes (width_mm/height_mm), repositions (hori_orient/vert_orient — friendly values like left/center/right/top/bottom work), and crops (crop_top_mm / crop_bottom_mm / crop_left_mm / crop_right_mm — mm trimmed per edge).
- To actually SEE an image (vision-capable models), call get_image — by graphic name, selection=true, or page=N to render that whole page. For a bulk read with pictures embedded, pass include_images=true to get_document_content."""
# App-neutral minimum (Calc/Draw sidebar prompts + the generic MCP topics via agent_manual):
# only rules that genuinely apply to every document type — no Writer tool names.
GENERIC_EDIT_CONFIRMATION_RULES = """EDITING THE DOCUMENT:
- Change the document with tools, not chat.
- VERIFY every edit by the tool result's structured fields: status='error' (or a zero count, where the tool reports counts) means nothing changed — do not assume success from friendly message wording.
- Any document content shown to you earlier may be a partial/truncated snapshot — before a targeted edit that depends on the exact current content, re-read through the tools."""
WRITER_CHAT_TOOLS_SECTION = """TOOLS:
- apply_document_content: Write HTML to the document (required after research delegates). See APPLY_DOCUMENT_CONTENT AND HTML below.
- get_document_content: Read document (full/selection/range) as HTML.
- search_in_document: Find text anywhere (body, tables, text boxes, shapes, headers/footers, comments); each match reports where it lives.
- apply_style: Apply a paragraph or character style (family='ParagraphStyles' or 'CharacterStyles').
- add_comment: Anchor review feedback or suggestions to a specific passage (see TOOL USAGE PATTERNS).
- get_guidance: Read the how-to manual on demand — no topic lists the topics; one topic (e.g. 'search', 'navigation', 'images') reads just that section."""
TRANSLATION_RULES = "TRANSLATION: get_document_content(scope=full) -> translate -> apply_document_content(target='full_document', content=translated). Do not use old_content or target='search' for whole-document translation. Never refuse."
# Tool-usage workflow patterns (no repeat of apply_document_content targets; see WRITER_APPLY_DOCUMENT_HTML_RULES).
# Shared piece: sidebar system prompt + MCP manual (agent_manual topic "editing").
TOOL_USAGE_PATTERNS = """TOOL USAGE PATTERNS:
- After an edit tool, confirm it landed via that tool's own structured field — not the message wording: apply_document_content -> replaced_count > 0 for search replaces (inserts — targets beginning/end/selection and position='before'/'after' — report status='ok', the latter with inserted=true); apply_style -> applied is true; add_comment -> comment_added is true. A no-op (e.g. text not found) returns status="error"; do not assume success.
- Any document text shown to you earlier may be a partial/truncated snapshot — before a targeted edit that depends on the exact current text, call get_document_content for the authoritative version.
- Successful apply_document_content edits also return edited_context — the touched paragraph(s) plus neighbors as they now read (in record/wait including the pending tracked change). Check it to confirm placement instead of an immediate re-read; full_document rewrites return no echo.
- apply_style applies formatting directly and is NOT recorded as a tracked change. When its result has style_unreviewed=true (review mode is on), briefly tell the user you changed a style, since they cannot accept or reject it the way they review your text edits.
- search_in_document (with return_offsets if needed) is for inspection/navigation; use apply_document_content with old_content for replacements.
- If a tool call fails, verify content and target are provided (use target='beginning' / 'end' / 'selection' for insert-only).
- When asked to review or give feedback or suggestions on a document, use the add_comment method to add your input to specific places in the document. Use for both positive and negative feedback.
- If the user says "fix this" (or a synonym or equivalent in another language with the same intent), assume they want you to correct spelling and grammar errors in the current sentence only, unless the context makes it clear there is another specific error they want you to fix."""
# apply_document_content only — design notes in docs/chat-sidebar-implementation.md § Chat prompt constants and docs/math-tex.md.
WRITER_APPLY_DOCUMENT_HTML_RULES = f"""APPLY_DOCUMENT_CONTENT AND HTML (CRITICAL):
- Parameters: `content` and `target` (required). If target='search', also `old_content` (a **substring** to find/replace; HTML in old_content is matched as plain text).
- **Whole-document replace:** use target='full_document' with `content` only. **Never** pass the entire document as old_content — that is not supported and will fail search.
- Targets: 'beginning', 'end', 'selection', 'full_document' (replaces all — preferred for rewrites/translations), or 'search' (substring find/replace only).
- With target='search', old_content may span multiple paragraphs (paragraph chaining), but each interior line must then match a WHOLE paragraph.
- position='before' / 'after' (with target='search') INSERTS the content next to the match and leaves the matched text untouched — the clean way to add a paragraph after a clause without re-sending the clause itself.
- Reach: edits cover body text, table cells, and text frames. Text inside a floating drawing shape is edited in place only when review mode is off — in record/wait it cannot become a tracked change, so the tool routes you to the shapes domain instead. Rich/block HTML inside a table cell is not supported (clear error, document untouched); use plain text or inline tags there.
- `content` must be a JSON array of HTML strings (one fragment per heading/paragraph). We wrap in <html>/<body>.
{HTML_FRAGMENT_RULES}
- Math: Use LaTeX inline delimiters \\(...\\) for math expressions (e.g. \\(x^2=4\\) or \\(a+b\\)); single variables (like x) can be plain text. No $, $$, \\[, HTML-escaped math, or equation images.
- Named paragraph styles: get_document_content marks each block's LibreOffice paragraph style as a `data-lo-style` token = the style name with spaces removed (e.g. `Heading 1`->`Heading1`, `Text body`->`Textbody`, `Caption`->`Caption`); use the tokens EXACTLY as returned. It reserves inline style="..." for direct character overrides. PRESERVE and USE it — emit `<p data-lo-style="Heading1">...</p>` to apply a named style, using the tokens exactly as returned (the named style is applied first, then any inline style="" is layered on top as a direct override). Prefer named styles over hardcoded inline formatting; an unknown token falls back to 'Standard'. data-lo-style is applied when you rewrite with target='full_document'; for targeted inserts/replaces (end/beginning/selection/search) the named style is NOT applied (it would restyle adjacent text) — rewrite via full_document for styling, or use apply_style to (re)style existing text. v1 limits: whole-paragraph alignment/colour/margins and table-cell styles do not round-trip; use named styles and span-level inline style for char exceptions (see docs/html_style_model_plan.md).
EXAMPLES:
- Good: ["<h1>Title</h1>", "<p>Paragraph with <strong>bold</strong> text and \\"quotes\\".</p>"]
- Good math: ["<p>The identity \\(a^2+b^2=c^2\\) holds.</p>"]
- Good styles: ["<p data-lo-style=\\"Heading1\\">Section title</p>", "<p data-lo-style=\\"Quotations\\">A quoted clause.</p>"]
- Bad: <h1>Title</h1><p>Paragraph</p> (must be a list of strings)
- Bad: ["<h1>Title</h1>"] (escaped entities)
- Bad: ["<math>x^2</math>"] (HTML-escaped math; use LaTeX delimiters)
- Bad: ["<p><img src=\\"...\\" alt=\\"equation\\"></p>"] (equation images; use LaTeX delimiters)
- Bad: ["# Title", "Paragraph"] (No Markdown)
- Bad: ["“Smart quotes”"] (use straight quotes ")"""
MEMORY_GUIDANCE = """MEMORY:
You have a persistent file-backed memory tool.
WHEN TO SAVE (do this proactively, don't wait to be asked):
- User corrects you.
- You discover something about the environment.
Prioritize what reduces future user steering."""
# Writer sidebar modes — not exposed on delegate_to_specialized_writer_toolset (user picks from dropdown).
WRITER_SIDEBAR_ONLY_DOMAINS = frozenset({"brainstorming", "writing_plan", "deep_research"})
# Impress/Draw sidebar modes — PPT-Master combo box; hidden from main chat and draw delegate.
IMPRESS_DRAW_SIDEBAR_ONLY_DOMAINS = frozenset({"ppt-master"})
# Single-line blocks: MCP tool descriptions and many clients do not render newlines inside JSON strings.
WRITER_SPECIALIZED_DELEGATION_TEMPLATE = (
"SPECIALIZED WRITER (nested tools): The default tool list hides deep Writer features. "
"When the user needs those, call delegate_to_specialized_writer_toolset with: domain one of: {domains} "
"and a `task` string that fully specifies what the specialized task must do. The task executor only sees tools for that domain, "
"but they are the real tools: **full parameter lists and full LibreOffice/UNO access** for that area (nothing is dumbed down for it). "
"document_research: use for information in other personal/business documents in the same folder (one delegation per file set). "
"web_research: public web topics; main agent writes returned report to document (apply_document_content). "
f"{SPECIALIZED_TASK_RULES}"
)
CALC_SPECIALIZED_DELEGATION_TEMPLATE = (
"SPECIALIZED CALC (nested tools): The default tool list hides advanced Calc features. "
"When the user needs those, call delegate_to_specialized_calc_toolset with: domain one of: {domains} "
"and a `task` string that fully specifies what the specialized task must do. The task executor has full tool access for that domain. "
f"{SPECIALIZED_TASK_RULES}"
)
DRAW_SPECIALIZED_DELEGATION_TEMPLATE = (
"SPECIALIZED DRAW (nested tools): The default tool list hides advanced Draw/Impress features. "
"When the user needs those, call delegate_to_specialized_draw_toolset with: domain one of: {domains} "
"and a `task` string that fully specifies what the specialized task must do. The task executor has full tool access for that domain. "
f"{SPECIALIZED_TASK_RULES}"
)
def _build_writer_chat_system_prompt_template() -> str:
"""Assemble Writer main-chat system prompt in model-facing order.
HYBRID delivery of the shared pieces: the ambient prompt carries the original pieces plus
the safety-critical review-modes piece (a model must know it may not resolve its own
tracked changes BEFORE it acts — weaker models never ask first); the reference pieces
(search, navigation, images) are pulled on demand through the get_guidance tool, so every
turn stays lean. The MCP-only extras (e.g. the HTTP 429 concurrency contract) stay out of
this ambient prompt — the sidebar runs in-process; if a sidebar model pulls the concurrency
topic anyway it just reads an inert rule."""
return "\n\n".join([
WRITER_CHAT_PERSONA,
CHAT_RESPONSE_FORMAT,
SIDEBAR_VS_DOCUMENT,
"{core_directives}",
WRITER_CHAT_TOOLS_SECTION,
TRANSLATION_RULES,
TOOL_USAGE_PATTERNS,
WRITER_REVIEW_MODES_RULES,
WRITER_APPLY_DOCUMENT_HTML_RULES,
"{specialized_delegation}",
MEMORY_GUIDANCE,
])
DEFAULT_CHAT_SYSTEM_PROMPT_TEMPLATE = _build_writer_chat_system_prompt_template()
# ---------------------------------------------------------------------------
# Writer sub-agents (assembly order differs — see docs/chat-sidebar-implementation.md § Chat prompt constants)
# ---------------------------------------------------------------------------
BRAINSTORMING_SUB_AGENT_INSTRUCTIONS = """BRAINSTORMING MODE:
You help turn ideas into fully formed designs through collaborative dialogue before any implementation.
HARD-GATE: Do NOT write code, scaffold features, or take implementation actions until the user has approved a design and you have saved it with save_design_spec.
ANTI-PATTERN — "This Is Too Simple To Need A Design" (too simple to design): Every idea/project goes through this design process. A todo list, a single-function utility, a config change — all of them. "Simple" projects are where unexamined assumptions cause the most wasted work. The design can be short (a few sentences for truly simple projects), but you MUST present it, get approval, and save it via save_design_spec.
SCOPE: Before detailed questions, check whether the request spans multiple independent subsystems. If so, say so in HTML, help decompose (independent pieces, how they relate, build order), and brainstorm ONE sub-project through this flow. Other pieces need their own spec cycles later.
WORKFLOW (in order):
1. Explore context: active document (get_document_content / get_document_tree), nearby files (list_nearby_files, grep_nearby_files, delegate_read_document), and public topics (brainstorm_research_web) when useful.
2. Ask clarifying questions — ONE question per reply_to_user call. Prefer multiple-choice when possible. Focus on purpose, constraints, and success criteria.
3. Propose 2–3 approaches with trade-offs as HTML (<ul> lists). Lead with your recommended option and why. Apply YAGNI — drop unnecessary features from every option.
4. Present the design in sections as HTML in reply_to_user. Cover when relevant: goals, architecture, components, data flow, error handling, testing. Scale each section: a few sentences if simple, up to ~200–300 words if nuanced. Ask after EACH section whether it looks right; be ready to revise earlier sections.
5. Spec self-review (internal, before save): review the HTML array you will pass to save_design_spec:
- Placeholder scan: Any "TBD", "TODO", incomplete sections, or vague requirements? Fix them.
- Internal consistency: Do any sections contradict each other? Does the architecture match the feature descriptions?
- Scope check: Is this focused enough for a single implementation plan, or does it need decomposition?
- Ambiguity check: Could any requirement be interpreted two different ways? If so, pick one and make it explicit.
Fix issues in the array before calling save_design_spec. Optionally summarize fixes in one HTML reply_to_user.
6. After full approval and self-review, call save_design_spec with the JSON array of HTML fragments (same rules as apply_document_content).
7. User review gate: reply_to_user with HTML like: <p>I've saved the design spec at the end of your document. Please read it there and tell me if you want any changes before implementation.</p> Wait for the user's response. If they request changes, revise in chat, re-run self-review, and save again (target end or full_document as appropriate).
8. Call brainstorming_finished with an HTML handoff message when the user approves the written spec to transition to implementation (the main chat will then invoke the writing-plans / implementation plan skill).
DESIGN QUALITY (Design for Isolation and Clarity):
- Break the system into smaller units that each have one clear purpose, communicate through well-defined interfaces, and can be understood and tested independently.
- For each unit, you should be able to answer: what does it do, how do you use it, and what does it depend on?
- Can someone understand what a unit does without reading its internals? Can you change the internals without breaking consumers? If not, the boundaries need work.
- In existing documents/codebases: explore structure first and follow established patterns. Where existing code has problems that affect the work (e.g. file too large, tangled responsibilities), include targeted improvements as part of the design. Do not propose unrelated refactoring; stay focused on what serves the current goal.
KEY PRINCIPLES:
- One question at a time; multiple choice preferred when possible.
- YAGNI ruthlessly; explore 2–3 alternatives before settling.
- Incremental validation: present design, get approval before moving on.
- Be flexible: go back and clarify when something does not make sense.
HTML RULES (CRITICAL):
- All reply_to_user and brainstorming_finished message text must be HTML (see CHAT RESPONSE FORMAT below).
- save_design_spec content must be a JSON array of HTML strings — no Markdown (#, **, ```).
- Do NOT use HTML entity escaping (<p>) — send real tags.
- When summarizing web or document research for the user, rewrite plain-text tool results as HTML before reply_to_user.
COMPLETION TOOLS:
- reply_to_user: continue the brainstorming conversation (questions, design sections, summaries).
- brainstorming_finished: END the session after the spec is saved and the user has reviewed it in the document.
- save_design_spec: the ONLY way to write to the document (never call apply_document_content)."""
WRITING_SUB_AGENT_INSTRUCTIONS = """WRITING PLAN MODE:
You help write documents collaboratively using a structured, plan-driven approach.
WORKFLOW (in order):
1. Explore context: read the active document (get_document_content / get_document_tree) or design spec to understand the user's goal, and search the public web using `writing_research_web` if needed to collect details.
2. Propose a structured Writing Plan/Outline - ONE outline of sections/headings as HTML. Ask the user if they want to modify the outline.
3. Keep the outline in the conversation history as a roadmap. Do NOT write the full outline/headings list to the document at the start (as headings will be written with section content and would appear twice).
4. Implement sections one-by-one:
- Generate high-quality content for a single section as HTML (including its heading).
- Insert it into the document using `write_document_section`.
- Ask the user for approval or feedback on the written section before moving to the next section.
5. Once all sections are written, call `writing_plan_finished` with a handoff message.
HTML RULES (CRITICAL):
- All reply_to_user and writing_plan_finished message text must be HTML.
- write_document_section content must be a JSON array of HTML strings — no Markdown (#, **, ```).
- Do NOT use HTML entity escaping (<p>) — send real tags.
COMPLETION TOOLS:
- reply_to_user: continue the writing plan conversation (questions, section drafts, summaries).
- writing_plan_finished: END the session after all sections are completed and reviewed.
- write_document_section: write content for a section to the document.
- writing_research_web: search the public web for context or information."""
DEEP_RESEARCH_SUB_AGENT_INSTRUCTIONS = """DEEP RESEARCH MODE:
You perform multi-step public web research and write formatted results into the active Writer document when appropriate.
WORKFLOW:
1. Read document context when helpful (get_document_content / get_document_tree / search_in_document).
2. Run deep_research_web for the user's research query. This may take several minutes (parallel searches, adaptive rounds).
3. Convert the plain-text report to HTML and insert it with apply_document_content (JSON array of HTML strings; target end unless the user asked otherwise). Do NOT paste the full report into reply_to_user.
4. reply_to_user with a brief HTML summary of what you researched and where it was inserted.
HTML RULES (CRITICAL):
- apply_document_content content must be a JSON array of HTML strings — no Markdown (#, **, ```).
- reply_to_user must be HTML and brief (status/summary only).
- Do NOT use HTML entity escaping (<p>) — send real tags.
- Rewrite plain-text deep_research_web results as structured HTML (headings, paragraphs, lists) before apply_document_content.
TOOLS:
- deep_research_web: multi-step adaptive web research only (not the shallow web_research tool).
- apply_document_content: the ONLY way to write research into the document.
- reply_to_user: short chat confirmation when the turn is complete."""
PPT_MASTER_SUB_AGENT_INSTRUCTIONS = """PPT-MASTER MODE (venv worker):
You run the upstream ppt-master workflow with filesystem + script access in the user Python venv.
WORKFLOW:
1. SKILL.md and routing files are pre-loaded; use read_ppt_master_workflow_file for references/ when needed.
2. Use run_ppt_master_script for upstream commands under scripts/ (project_manager, pdf_to_md, svg_to_pptx, etc.).
3. Use read_project_file / write_project_file for project artifacts (svg_output/, design_spec.md, …).
4. When exports are ready, call export_presentation_project on the host to import into the active Impress/Draw document.
5. validate_ppt_master_project checks project artifacts before export.
6. apply_ppt_master_template_fill and apply_ppt_master_native_enhance for template-fill and enhancement routes.
REQUIREMENTS:
- Configured user Python venv with ppt-master requirements.txt installed.
- PPT-Master data path must contain SKILL.md and scripts/.
HTML RULES:
- reply_to_user and ppt_master_finished messages must be HTML (see CHAT RESPONSE FORMAT).
COMPLETION:
- reply_to_user: continue the PPT-Master session.
- ppt_master_finished: end when the deck is done or the user switches back to Chat mode. Set exported=true if export_presentation_project succeeded."""
def get_ppt_master_sub_agent_instructions(ctx=None) -> str:
"""Full system instructions for the PPT-Master smol sub-agent (Impress/Draw sidebar)."""
parts = [
PPT_MASTER_SUB_AGENT_INSTRUCTIONS,
get_chat_response_format_instructions(ctx),
]
return "\n\n".join(parts)
def get_brainstorming_sub_agent_instructions(ctx=None) -> str:
"""Full system instructions for the brainstorming smol sub-agent."""
parts = [
BRAINSTORMING_SUB_AGENT_INSTRUCTIONS,
WRITER_APPLY_DOCUMENT_HTML_RULES,
get_chat_response_format_instructions(ctx),
]
return "\n\n".join(parts)
def get_deep_research_sub_agent_instructions(ctx=None) -> str:
"""Full system instructions for the Deep Research smol sub-agent (sidebar)."""
parts = [
DEEP_RESEARCH_SUB_AGENT_INSTRUCTIONS,
WRITER_APPLY_DOCUMENT_HTML_RULES,
get_chat_response_format_instructions(ctx),
]
return "\n\n".join(parts)
def get_writing_sub_agent_instructions(ctx=None) -> str:
"""Full system instructions for the writing plan smol sub-agent."""
parts = [
WRITING_SUB_AGENT_INSTRUCTIONS,
WRITER_APPLY_DOCUMENT_HTML_RULES,
get_chat_response_format_instructions(ctx),
]
return "\n\n".join(parts)
# Web-research sub-agent only (main chat delegate + web-research checkbox). Facts in plain text;
# main agent applies HTML, memory colors, and apply_document_content when the user wanted a doc edit.
WEB_RESEARCH_PLAIN_TEXT_FORMAT = """Research output: plain text only in final_answer.
- Use clear section headings (plain lines) and bullet lists (- item).
- Include facts, names, dates, ratings, and sources where relevant.
- No HTML tags, no Markdown (# or **), no JSON."""
# ---------------------------------------------------------------------------
# Calc / Draw chat system prompts
# ---------------------------------------------------------------------------
CALC_WORKFLOW = """WORKFLOW:
1. Understand what the user wants.
2. If needed, use get_sheet_summary or read_cell_range to see the current state.
3. Use the tools to perform the operation. Always use ranges for multiple cells to reduce calls and improve efficiency.
4. Give a short confirmation; when you changed cells, mention the range or addresses (e.g. "Wrote totals in B5:B8")."""
# Shared venv Python prompt text (run_venv_python_script, =PY(), delegate domain=python).
PYTHON_VENV_AUTO_IMPORTS_ALIASES = "`numpy` (as `np`), `sympy` (as `sp`), `pandas` (as `pd`), `scipy.stats` (as `st`), `matplotlib.pyplot` (as `plt`), `plugin.scripting.calc_functions` (as `calc`), standard library `math`, `datetime` (as `dt`), `re`, `random`, `statistics`, `collections`, `itertools`, `json`, and `csv`. When `=PY` has data range args, a binding-only `xl(\"%Pn%\")` helper is also injected (Excel import; not a live sheet read)"
# Populated at module end (after full constants init) to avoid import cycles via smolagents.
_VENV_IMPORT_POLICY_COMPACT = ""
_VENV_IMPORT_POLICY_FULL = ""
PYTHON_VENV_AUTO_IMPORTS_TOOL_NOTE = ""
PYTHON_VENV_AUTO_IMPORTS_PROMPT_LINE = ""
# Used when the LLM should emit =PY formulas (chat / specialized python paths). Not the =PROMPT() default.
CALC_PYTHON_FORMULA_LLM_HINT = ""
# Default system prompt for Calc =PROMPT() when systemPrompt arg and extend_selection_system_prompt are empty.
CALC_PROMPT_CELL_SYSTEM_PROMPT = (
"Answer the user's request directly in plain text suitable for a spreadsheet cell. "
"Do not use HTML or markdown fences unless the user asks for them."
)
def python_specialized_sub_agent_hint(agent_label: str) -> str:
"""Smol sub-agent instructions suffix for delegate_to_specialized_* (domain=\"python\")."""
if agent_label == "Calc":
data_hint = (
" For bulk data use data_range with run_venv_python_script "
"(one A1 address, comma-separated addresses, or an array); "
"`data` is the range when one address is passed, or the same list as `ranges` when several are. "
"The host resolves addresses out-of-band. Avoid passing large values in the data parameter."
)
else:
data_hint = " run_venv_python_script does not inject spreadsheet `data`—use document tools for content."
policy = _VENV_IMPORT_POLICY_FULL or _load_venv_import_policy_full()
from plugin.scripting.import_policy import format_matplotlib_plot_hint, format_units_helper_hint
plot_hint = format_matplotlib_plot_hint(agent_label=agent_label)
plot_suffix = f" {plot_hint}" if plot_hint else ""
units_hint = format_units_helper_hint()
return (
f" PYTHON (venv): {policy}{data_hint}{plot_suffix}"
" Prefer symbolic_math for solve/simplify/integrate/differentiate over raw sp/run_venv_python_script."
f" {units_hint}"
)
def _load_venv_import_policy_full() -> str:
from plugin.scripting.import_policy import format_venv_import_policy_for_prompt
return format_venv_import_policy_for_prompt(compact=False)
CALC_FORMULA_SYNTAX = """FORMULA SYNTAX: LibreOffice uses semicolon (;) as the formula argument separator in formulas.
- Correct: =SUM(A1:A10), =IF(A1>0;B1;C1)
- Wrong: =SUM(A1,A10), =IF(A1>0,"Yes","No") (no commas in formulas)
- Write `=PY("result = ..."; A1:A10)` in cells to calculate/run Python (omit the second argument if no data is needed, e.g. `=PY("result = 2**10")`).
- Example: `=PY("result = np.sum(data)"; A1:A10)`."""
# DEFAULT_CALC_CHAT_SYSTEM_PROMPT_TEMPLATE is built in _init_venv_import_policy_strings() (needs import policy).
DEFAULT_CALC_CHAT_SYSTEM_PROMPT_TEMPLATE = ""
DEFAULT_DRAW_CHAT_SYSTEM_PROMPT_TEMPLATE = """You are a LibreOffice Draw/Impress assistant who creates polished, professional, and colorful visual content.
Do not explain - do the operation directly using tools. Perform as many steps as needed in one turn when possible.
""" + CHAT_RESPONSE_FORMAT + """
""" + GENERIC_EDIT_CONFIRMATION_RULES + """
WORKFLOW:
1. Understand the user's request.
2. If needed, use list_pages to understand the current layout.
3. Use the specialized delegation tool to perform shape operations (create, edit, group, etc.).
4. Give a short confirmation; when you changed pages/shapes, mention them.
CSV DATA: Use comma (,) for write_formula_range.
TOOLS (grouped by use):
READ:
- list_pages: List pages in the document.
- get_page_summary: Summary of shapes on the active/specified page (types, names, layout).
- get_document_tree: Outline tree of pages, slides, and shapes.
WRITE:
- delete_structure: Remove pages or shapes by index or ID.
{specialized_delegation}
{core_directives}"""
# ---------------------------------------------------------------------------
# Globals resolved dynamically (main-chat, Calc, Draw defaults)
# ---------------------------------------------------------------------------
DEFAULT_CHAT_SYSTEM_PROMPT = ""
DEFAULT_CALC_CHAT_SYSTEM_PROMPT = ""
DEFAULT_DRAW_CHAT_SYSTEM_PROMPT = ""
def _catalog_entries_from_base(base_cls, *, agent_label: str | None = None, ctx=None) -> list[dict[str, str]]:
"""Build ``[{domain, description}, …]`` for one specialized base class (delegate/MCP catalog)."""
entries: list[dict[str, str]] = []
for cls in base_cls.__subclasses__():
domain = getattr(cls, "specialized_domain", None)
desc = getattr(cls, "specialized_domain_description", None)
if not domain:
continue
if agent_label == "Calc" and domain == "python":
continue
if agent_label == "Writer" and domain in WRITER_SIDEBAR_ONLY_DOMAINS:
continue
if agent_label == "Draw" and domain in IMPRESS_DRAW_SIDEBAR_ONLY_DOMAINS:
continue
if domain == "vision" and ctx is not None:
from plugin.vision.vision_availability import vision_venv_configured
if not vision_venv_configured(ctx):
continue
entries.append({"domain": str(domain), "description": str(desc or "")})
return entries
def get_specialized_domain_catalog(*, agent_label: str | None, ctx=None) -> list[dict[str, str]]:
"""Full specialized domain catalog — same entries as sidebar/delegate domain hints.
``agent_label`` is ``Writer`` / ``Calc`` / ``Draw`` for one app, or ``None`` to merge
all three (e.g. MCP ``find_tools`` with no document open).
"""
if agent_label == "Calc":
from plugin.calc.base import ToolCalcSpecialBase
entries = _catalog_entries_from_base(ToolCalcSpecialBase, agent_label="Calc", ctx=ctx)
elif agent_label == "Draw":
from plugin.draw.base import ToolDrawSpecialBase
entries = _catalog_entries_from_base(ToolDrawSpecialBase, agent_label="Draw", ctx=ctx)
elif agent_label == "Writer":
from plugin.writer.specialized_base import ToolWriterSpecialBase
entries = _catalog_entries_from_base(ToolWriterSpecialBase, agent_label="Writer", ctx=ctx)
else:
from plugin.calc.base import ToolCalcSpecialBase
from plugin.draw.base import ToolDrawSpecialBase
from plugin.writer.specialized_base import ToolWriterSpecialBase
seen: dict[str, str] = {}
for base, label in (
(ToolWriterSpecialBase, "Writer"),
(ToolCalcSpecialBase, "Calc"),
(ToolDrawSpecialBase, "Draw"),
):
for entry in _catalog_entries_from_base(base, agent_label=label, ctx=ctx):
dom = entry["domain"]
desc = entry["description"]
if dom not in seen or len(desc) > len(seen[dom]):
seen[dom] = desc
return [{"domain": dom, "description": seen[dom]} for dom in sorted(seen)]
entries.sort(key=lambda e: e["domain"])
return entries
def _get_specialized_domains_str(base_cls, *, agent_label: str | None = None, ctx=None) -> str:
"""Build a compact domain list for delegation hints and MCP schemas."""
parts = []
for entry in sorted(_catalog_entries_from_base(base_cls, agent_label=agent_label, ctx=ctx),
key=lambda e: e["domain"]):
if entry["description"]:
parts.append(f"{entry['domain']}: {entry['description']}")
else:
parts.append(entry["domain"])
return "; ".join(parts)
def _specialized_delegation_template_for_label(agent_label: str) -> str:
if agent_label == "Calc":
return CALC_SPECIALIZED_DELEGATION_TEMPLATE
if agent_label == "Draw":
return DRAW_SPECIALIZED_DELEGATION_TEMPLATE
return WRITER_SPECIALIZED_DELEGATION_TEMPLATE
def get_specialized_delegation_for_model(model, ctx=None) -> str:
"""Specialized-delegation block for chat system prompt (same text as MCP delegate tool hint)."""
from plugin.doc.document_helpers import is_calc, is_draw
if is_calc(model):
from plugin.calc.base import ToolCalcSpecialBase
return get_specialized_delegation_tool_hint(ToolCalcSpecialBase, "Calc", ctx=ctx)
if is_draw(model):
from plugin.draw.base import ToolDrawSpecialBase
return get_specialized_delegation_tool_hint(ToolDrawSpecialBase, "Draw", ctx=ctx)
from plugin.writer.specialized_base import ToolWriterSpecialBase
return get_specialized_delegation_tool_hint(ToolWriterSpecialBase, "Writer", ctx=ctx)
def format_specialized_domains_description(special_base_class, *, agent_label: str | None = None, ctx=None) -> str:
"""Domain enum help for MCP/OpenAPI (more compact than the full delegation hint)."""
domains = _get_specialized_domains_str(special_base_class, agent_label=agent_label, ctx=ctx)
if not domains:
return "The specialized domain to activate."
# Compact form for the enum property description to reduce bloat in MCP schema
compact = domains.replace("; ", ", ")
return f"domain one of: {compact}"
def get_specialized_delegation_tool_hint(special_base_class, agent_label: str, *, ctx=None) -> str:
"""Full specialized-delegation guidance (sidebar system prompt and MCP ``tools/list``)."""
domains_str = _get_specialized_domains_str(special_base_class, agent_label=agent_label, ctx=ctx)
template = _specialized_delegation_template_for_label(agent_label)
return template.format(domains=domains_str)
def get_vision_core_directive(model, ctx) -> str:
"""OCR delegation hint when local vision stack is configured (Writer/Calc only)."""
if ctx is None:
return ""
from plugin.doc.document_helpers import is_calc, is_writer
from plugin.vision.vision_availability import vision_venv_configured
if not vision_venv_configured(ctx):
return ""
if not (is_writer(model) or is_calc(model)):
return ""
delegate = "delegate_to_specialized_calc_toolset" if is_calc(model) else "delegate_to_specialized_writer_toolset"
return (
f"When the user wants OCR or text from an embedded image, {delegate}(domain=\"vision\", task=\"\"). "
"That runs local OCR on the selected graphic and inserts the recognized text into the document "
"(no sub-agent; task is ignored). You must use this call to perform OCR."
)
DEFAULT_WRITER_GREETING = "AI: I can edit or translate your document instantly with professional formatting and color. Try me!"
DEFAULT_CALC_GREETING = "AI: I can help you with formulas, data analysis, and colorful charts. Try me!"
DEFAULT_DRAW_GREETING = "AI: I can help you create and edit polished, colorful shapes in Draw and Impress. Try me!"
DEFAULT_RESEARCH_GREETING = "AI: I can do web research to answer any question, or summarize a web page, without seeing or changing your document. Let's chat."
DEFAULT_DEEP_RESEARCH_GREETING = "AI: Deep Research mode runs a multi-step web investigation (planning, several searches, synthesis) and can insert a formatted report into your document. It takes longer but produces more thorough results."
DEFAULT_BRAINSTORMING_GREETING = "AI: Let's explore and design your idea together. I'll ask questions, suggest approaches, and help you build an approved spec in your document when you're ready."
DEFAULT_WRITING_PLAN_GREETING = "AI: Let's draft your document section-by-section. I'll help you create a writing plan outline, and then implement it incrementally with your approval."
DEFAULT_PPT_MASTER_GREETING = "AI: PPT-Master mode — I'll run the ppt-master workflow in your configured Python venv (scripts + export to Impress). Describe your topic or point me at a project folder."
def get_greeting_for_document(model):
"""Return a greeting relevant to the document type."""
from plugin.framework.i18n import _
from plugin.doc.document_helpers import is_calc, is_draw
if is_calc(model):
return _(DEFAULT_CALC_GREETING)
elif is_draw(model):
return _(DEFAULT_DRAW_GREETING)
else:
return _(DEFAULT_WRITER_GREETING)
def get_chat_system_prompt_for_document(model, additional_instructions="", ctx=None):
"""Single source of truth for chat system prompt. Use this so Writer vs Calc prompt cannot be mixed.
model: document model (Writer, Calc, or Draw). additional_instructions: optional extra text appended.
Callers must pass the document that is being chatted about."""
from plugin.doc.document_helpers import is_calc, is_draw
delegation = get_specialized_delegation_for_model(model, ctx=ctx)
if is_calc(model):
base = DEFAULT_CALC_CHAT_SYSTEM_PROMPT_TEMPLATE.replace("{specialized_delegation}", delegation)
base = base.replace("{core_directives}", CALC_CORE_DIRECTIVES)
global DEFAULT_CALC_CHAT_SYSTEM_PROMPT
if not DEFAULT_CALC_CHAT_SYSTEM_PROMPT:
DEFAULT_CALC_CHAT_SYSTEM_PROMPT = base
elif is_draw(model):
base = DEFAULT_DRAW_CHAT_SYSTEM_PROMPT_TEMPLATE.replace("{specialized_delegation}", delegation)
base = base.replace("{core_directives}", DRAW_CORE_DIRECTIVES)
global DEFAULT_DRAW_CHAT_SYSTEM_PROMPT
if not DEFAULT_DRAW_CHAT_SYSTEM_PROMPT:
DEFAULT_DRAW_CHAT_SYSTEM_PROMPT = base
else:
base = DEFAULT_CHAT_SYSTEM_PROMPT_TEMPLATE.replace("{specialized_delegation}", delegation)
base = base.replace("{core_directives}", WRITER_CORE_DIRECTIVES)
# update the static variable once it's lazily generated so tests and imports works
global DEFAULT_CHAT_SYSTEM_PROMPT
if not DEFAULT_CHAT_SYSTEM_PROMPT:
DEFAULT_CHAT_SYSTEM_PROMPT = base
base = base.replace(CHAT_RESPONSE_FORMAT, get_chat_response_format_instructions(ctx))
vision_directive = get_vision_core_directive(model, ctx)
if vision_directive:
base += "\n\n" + vision_directive
if ctx:
try:
from plugin.chatbot.memory import MemoryStore
store = MemoryStore(ctx)
user_mem = store.read("user")
if user_mem:
base += "\n\n[USER PROFILE / MEMORY]\n" + user_mem.strip() + "\n"
except Exception as e:
import logging
logging.getLogger(__name__).debug(f"Failed to read user memory for prompt: {e}")
# Humanizer skill (minimal addition, re-uses the exact same injection pattern as memory above).
# When enabled, the model receives the rules as ambient context for any prose it generates
# or revises. This is the primary delivery mechanism — cheap, always-on when active,
# and automatically benefits main chat + all writing/brainstorming sub-agents.
# User can turn it off in Settings or override the rules by editing the SKILL.md file.
# Defined in plugin/chatbot/module.yaml so it appears as a checkbox in the sidebar Settings.
try:
from plugin.chatbot.skills import SkillStore
from plugin.framework.config import get_config_bool_safe
if get_config_bool_safe("chatbot.humanizer_enabled"):
hstore = SkillStore(ctx)
hguidance = hstore.get_humanizer_guidance()
if hguidance:
base += "\n\n[HUMANIZER GUIDANCE — apply when generating or revising prose]\n" + hguidance.strip() + "\n"
except Exception as e:
import logging
logging.getLogger(__name__).debug(f"Failed to inject humanizer guidance: {e}")
if additional_instructions and str(additional_instructions).strip():
return base + "\n\n" + str(additional_instructions).strip()
return base
WRITER_EVAL_TOOLS_SECTION = """TOOLS (eval harness):
- apply_document_content: Insert or replace HTML in the document (parameters and format — see APPLY_DOCUMENT_CONTENT AND HTML below).
- get_document_content: Read document (full/selection/range) as HTML.
- find_text: Find text in the document (JSON ranges)."""
WRITER_EVAL_SCOPE = (
"[Eval harness] Only get_document_content, apply_document_content, and find_text are registered. "
"Do not use web research, delegate_to_specialized_writer_toolset, search_in_document, apply_style, or add_comment."
)
WRITER_EVAL_TOOL_USAGE_PATTERNS = """TOOL USAGE PATTERNS (eval harness):
- Use find_text to locate passages; use apply_document_content (often with old_content) to replace HTML.
- Re-read with get_document_content after substantive edits if needed."""
def get_writer_eval_chat_system_prompt() -> str:
"""Writer chat-style system prompt for offline DSPy eval (`scripts/prompt_optimization`).
Reuses the same HTML / apply_document_content rules as production chat
(`WRITER_APPLY_DOCUMENT_HTML_RULES`, `TRANSLATION_RULES`) but describes only tools implemented in the
eval harness: ``get_document_content``, ``apply_document_content``, ``find_text``.
Omits web research, specialized delegation, memory, and tools not wired in ``tools_lo``.
"""
return "\n\n".join([
SIDEBAR_VS_DOCUMENT,
WRITER_EVAL_SCOPE,
WRITER_EVAL_TOOLS_SECTION,
TRANSLATION_RULES,
WRITER_EVAL_TOOL_USAGE_PATTERNS,
WRITER_APPLY_DOCUMENT_HTML_RULES,
])
def _init_venv_import_policy_strings() -> None:
"""Late init: import_policy pulls smolagents; constants must be fully loaded first."""
global _VENV_IMPORT_POLICY_COMPACT, _VENV_IMPORT_POLICY_FULL
global PYTHON_VENV_AUTO_IMPORTS_TOOL_NOTE, PYTHON_VENV_AUTO_IMPORTS_PROMPT_LINE
global CALC_FORMULA_SYNTAX, DEFAULT_CALC_CHAT_SYSTEM_PROMPT_TEMPLATE, CALC_PYTHON_FORMULA_LLM_HINT
from plugin.scripting.import_policy import format_matplotlib_plot_hint, format_venv_import_policy_for_prompt
compact = format_venv_import_policy_for_prompt(compact=True)
_VENV_IMPORT_POLICY_COMPACT = compact
_VENV_IMPORT_POLICY_FULL = format_venv_import_policy_for_prompt(compact=False)
PYTHON_VENV_AUTO_IMPORTS_TOOL_NOTE = compact + " "
PYTHON_VENV_AUTO_IMPORTS_PROMPT_LINE = compact
calc_plot_hint = format_matplotlib_plot_hint(doc_type="calc")
CALC_PYTHON_FORMULA_LLM_HINT = (
compact
+ " When outputting Calc Python formulas: write `=PY(\"result = …\"; A1:A10)`. "
"Use semicolon (;) argument separators; code runs in the venv sandbox above."
+ (f" {calc_plot_hint}" if calc_plot_hint else "")
)
CALC_FORMULA_SYNTAX = f"""FORMULA SYNTAX: LibreOffice uses semicolon (;) as the formula argument separator in formulas.
- Correct: =SUM(A1:A10), =IF(A1>0;B1;C1)
- Wrong: =SUM(A1,A10), =IF(A1>0,"Yes","No") (no commas in formulas)
- Write `=PY("result = ..."; A1:A10)` in cells to calculate/run Python (omit the second argument if no data is needed, e.g. `=PY("result = 2**10")`).
{compact}
- Example: `=PY("result = np.sum(data)"; A1:A10)`."""
DEFAULT_CALC_CHAT_SYSTEM_PROMPT_TEMPLATE = f"""You are a LibreOffice Calc spreadsheet assistant who creates polished, professional, and colorful spreadsheets.
Do not explain, do the operation directly using tools. Perform as many steps as needed in one turn when possible.
{CHAT_RESPONSE_FORMAT}
{GENERIC_EDIT_CONFIRMATION_RULES}
{CALC_WORKFLOW}
{CALC_FORMULA_SYNTAX}
CSV DATA: Use comma (,) for write_formula_range.
CELL LINKS: Reference cells with HTML only, e.g. <a href="cell://B2">B2</a> (users click these in the chat sidebar to jump to the cell).
TOOLS (grouped by use):
READ:
- read_cell_range: Read values from a cell or range (e.g. A1:D10). Date/time cells return ISO in value (date/time/datetime) plus format_code (display FormatString, observability only); elapsed formats return PTnHnMnS (e.g. PT30H) as type duration.
- get_sheet_summary: Summary of the active sheet (size, headers, used range, charts, annotations, merges).
WRITE & FORMAT:
- write_formula_range: Single string fills entire range; JSON array must match range size exactly (one value per cell). Alternatively, provide multiline CSV data to bulk insert starting at a cell. Use empty string/array to clear contents. Use ranges for efficiency; avoid single-cell operations. Prefer plain values/ISO dates for static cells; use '=' formulas only when the cell must stay live. Dates/times: ISO only (YYYY-MM-DD, HH:MM[:SS], YYYY-MM-DDTHH:MM[:SS]); elapsed: PTnHnMnS (e.g. PT30H). No timezone offset/Z or locale forms like 08/05/2026.
- set_style: Use for one or more cells/ranges at once (same formatting applied per range). Good after bulk writes for uniform look. It only exposes a small fixed set of properties (see list below)—not mixed rich text inside a cell. For per-character formatting, links, or HTML structure in a single cell, use insert_cell_html instead.
- set_style properties (each optional except range_name): range_name (array of addresses/ranges); bold; italic; font_size (points); bg_color; font_color (hex #RRGGBB or names: red, yellow, …); h_align (left|center|right|justify); v_align (top|center|bottom); wrap_text; border_color (outline around the range).
- insert_cell_html: Paste HTML into one cell on the active sheet as rich text (bold, italic, links, line breaks—same import as Writer). Plain write_formula_range cannot do this. One cell only; no images. Does not replace set_style for whole-table borders—combine as needed.
- merge_cells: Merge a range (e.g. headers); then write and style with write_formula_range/set_style.
- delete_structure: Remove rows or columns at specific positions.
{{specialized_delegation}}
{{core_directives}}"""
_init_venv_import_policy_strings()