Repository navigation
Expand file tree
/
Copy pathanalyzer.py
More file actions
215 lines (178 loc) · 9.19 KB
/
Copy pathanalyzer.py
File metadata and controls
215 lines (178 loc) · 9.19 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
# WriterAgent - AI Writing Assistant for LibreOffice
# Copyright (c) 2024 John Balis
# Copyright (c) 2026 KeithCu (modifications and relicensing)
# Copyright (c) 2026 LibreCalc AI Assistant (Calc integration features, originally MIT)
#
# 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/>.
"""Sheet analyzer — analyses the structure and statistics of Calc sheets.
Ported from core/calc_sheet_analyzer.py for the plugin framework.
"""
from __future__ import annotations
import logging
from typing import Any
from plugin.framework.errors import ToolExecutionError, UnoObjectError, check_disposed, is_disposed_exception, safe_call
from plugin.framework.thread_guard import main_thread_only
from plugin.calc.address_utils import index_to_column
from plugin.calc.bridge import filter_agent_sheet_names, is_agent_visible_sheet
log = logging.getLogger("writeragent.calc")
class SheetAnalyzer:
"""Analyses the structure and data of a worksheet."""
bridge: Any
def __init__(self, bridge: Any) -> None:
"""
Args:
bridge: CalcBridge instance.
"""
self.bridge = bridge
def get_sheet_summary(self, sheet_name: str | None = None) -> dict[str, Any]:
"""Return a general summary of the active or specified sheet.
Args:
sheet_name: Optional name of the sheet to analyse.
Returns:
dict with keys: sheet_name, used_range, row_count, col_count,
headers.
"""
try:
if sheet_name:
doc = self.bridge.get_active_document()
sheet = doc.getSheets().getByName(sheet_name)
else:
sheet = self.bridge.get_active_sheet()
cursor = sheet.createCursor()
cursor.gotoStartOfUsedArea(False)
cursor.gotoEndOfUsedArea(True)
range_addr = cursor.getRangeAddress()
start_col = range_addr.StartColumn
start_row = range_addr.StartRow
end_col = range_addr.EndColumn
end_row = range_addr.EndRow
row_count = end_row - start_row + 1
col_count = end_col - start_col + 1
start_col_str = index_to_column(start_col)
end_col_str = index_to_column(end_col)
used_range = f"{start_col_str}{start_row + 1}:{end_col_str}{end_row + 1}"
header_range = sheet.getCellRangeByPosition(start_col, start_row, end_col, start_row)
header_data = header_range.getDataArray()
headers = []
if header_data and len(header_data) > 0:
for val in header_data[0]:
# getDataArray returns floats for numbers and strings for text.
# Convert float to string if needed, empty strings to None.
str_val = str(val) if val != "" else None
headers.append(str_val)
result = {"sheet_name": sheet.getName(), "used_range": used_range, "row_count": row_count, "col_count": col_count, "headers": headers}
# Charts
try:
charts = sheet.getCharts()
result["chart_count"] = charts.getCount()
result["charts"] = list(charts.getElementNames())
except Exception as e:
log.debug("get_sheet_summary charts error: %s", e)
result["chart_count"] = 0
result["charts"] = []
# Annotations
try:
result["annotation_count"] = sheet.getAnnotations().getCount()
except Exception as e:
log.debug("get_sheet_summary annotations error: %s", e)
result["annotation_count"] = 0
# Merged cells - count via querying
try:
result["has_merges"] = sheet.getPropertyValue("HasMergedCells") if hasattr(sheet, "getPropertyValue") else None
except Exception as e:
log.debug("get_sheet_summary merged cells error: %s", e)
result["has_merges"] = None
# Draw page (shapes on sheet)
try:
dp = sheet.DrawPage
result["shape_count"] = dp.getCount()
except Exception as e:
log.debug("get_sheet_summary shape_count error: %s", e)
result["shape_count"] = 0
return result
except Exception as e:
# Re-raise disposal. Wrapping DisposedException as
# ToolExecutionError hides it from get_calc_context_for_chat and
# from get_sheet_summary (execute_safe then reports
# TOOL_EXECUTION_ERROR).
if is_disposed_exception(e):
raise
log.exception("Error creating sheet summary")
raise ToolExecutionError(str(e)) from e
@main_thread_only
def get_calc_context_for_chat(model: Any, max_context: int = 8000, ctx: Any = None) -> str:
"""Get context summary for a Calc spreadsheet."""
if ctx is None:
raise ValueError("ctx is required for get_calc_context_for_chat")
try:
from plugin.calc.bridge import CalcBridge
check_disposed(model, "Document Model")
bridge = CalcBridge(model)
analyzer = SheetAnalyzer(bridge)
summary = analyzer.get_sheet_summary()
ctx_str = f"Spreadsheet Document: {model.getURL() or 'Untitled'}\n"
sheets = model.getSheets()
# Omit LO internals (leading '_', e.g. __Anonymous_Sheet_DB__0) from chat.
sheet_names = filter_agent_sheet_names(sheets.getElementNames())
ctx_str += f"Sheets: {sheet_names}\n"
if not is_agent_visible_sheet(summary["sheet_name"]):
if sheet_names:
summary = analyzer.get_sheet_summary(sheet_name=sheet_names[0])
else:
return ctx_str + "No user-visible sheets.\n"
ctx_str += f"Active Sheet: {summary['sheet_name']}\n"
ctx_str += f"Used Range: {summary['used_range']} ({summary['row_count']} rows x {summary['col_count']} columns)\n"
ctx_str += f"Columns: {', '.join([str(h) for h in summary['headers'] if h])}\n"
# Add selection context if available
controller = safe_call(model.getCurrentController, "Get current controller")
selection = safe_call(controller.getSelection, "Get selection")
if selection:
if hasattr(selection, "getRangeAddress"):
addr = safe_call(selection.getRangeAddress, "Get range address")
sel_range = f"{index_to_column(addr.StartColumn)}{addr.StartRow + 1}:{index_to_column(addr.EndColumn)}{addr.EndRow + 1}"
ctx_str += f"Current Selection: {sel_range}\n"
# Check for selected values if small
if (addr.EndRow - addr.StartRow + 1) * (addr.EndColumn - addr.StartColumn + 1) < 100:
from plugin.calc.inspector import CellInspector
inspector = CellInspector(bridge)
# LLM-facing context: enrich dates like read_cell_range (#374 Bug 3).
cells = inspector.read_range(sel_range, include_format_info=True)
ctx_str += "Selection Content (CSV-like):\n"
for row in cells:
ctx_str += ", ".join([str(c["value"]) if c["value"] is not None else "" for c in row]) + "\n"
return ctx_str
except UnoObjectError:
logging.getLogger(__name__).exception("get_calc_context_for_chat error")
return "[Unable to read Calc spreadsheet context. The document may be locked or initializing.]"
except Exception:
logging.getLogger(__name__).exception("get_calc_context_for_chat exception")
return "[Unable to read Calc spreadsheet context. The document may be locked or initializing.]"
def get_full_calc_text(model: Any, max_chars: int = 8000) -> str:
"""Short active-sheet summary for ``get_full_document_text`` (not the chat excerpt).
Kept next to ``get_calc_context_for_chat`` so ``document_helpers`` can lazy-import
this module instead of constructing ``SheetAnalyzer`` itself. ``max_chars`` is
unused; the summary is already a few lines.
"""
from plugin.calc.bridge import CalcBridge
bridge = CalcBridge(model)
analyzer = SheetAnalyzer(bridge)
summary = analyzer.get_sheet_summary()
if not is_agent_visible_sheet(summary["sheet_name"]):
visible = filter_agent_sheet_names(model.getSheets().getElementNames())
if not visible:
return "No user-visible sheets.\n"
summary = analyzer.get_sheet_summary(sheet_name=visible[0])
text = f"Sheet: {summary['sheet_name']}\nUsed Range: {summary['used_range']}\n"
text += f"Columns: {', '.join(filter(None, summary['headers']))}\n"
return text