We all deal with spreadsheets. They’re everywhere, financial reports, sales data, operational metrics. But raw data in a flat table is just that: raw data. To extract insights, you need dashboards, charts, KPIs, conditional formatting, and executive summaries. Doing this manually is tedious. What if an AI agent could take any raw .xlsx file and transform it into a professional, multi-sheet workbook with formulas, charts, and insights, automatically?

That’s exactly what this project does. The idea is simple: you give it a spreadsheet, and an AI agent running Python inside a AWS sandbox analyzes the data, builds a Dashboard with KPI formulas, formats the source data, generates an executive summary with real insights, and creates analysis sheets with charts, all using Excel formulas, never hardcoded values.

The two-agent pattern
The core of the system is a two-agent architecture. An outer orchestrator agent (Claude Sonnet) manages the workflow, while an inner agent (Claude Opus) does the actual Excel work inside an AWS Bedrock Code Interpreter sandbox. This separation keeps the orchestration clean and lets the inner agent focus entirely on writing Python code with openpyxl.
The CLI entry point uses Click. When you run the command, it creates the orchestrator agent with the xlsx_enhancer tool:
click.command()click.argument("input_file", type=click.Path(exists=True))click.argument("output_file", type=click.Path(), required=False)def run(input_file: str, output_file: str | None): if not output_file: p = Path(input_file) output_file = str(p.parent / f"enhanced_{p.name}") agent = create_agent( system_prompt=ORCHESTRATOR_PROMPT, tools=[xlsx_enhancer], hooks=[ToolProgressHook()], ) response = agent( f"Process the Excel file at {input_file} and save the enhanced version to {output_file}" ) click.echo(f"Done: {str(response)}")
The agent factory wraps the Strands SDK configuration, model selection, retry logic, sliding window conversation management:
def create_agent( system_prompt: str, model: str = Models.CLAUDE_45, tools: Optional[List[Any]] = None, hooks: Optional[List[HookProvider]] = None, temperature: float = 0.3, read_timeout: int = 300, connect_timeout: int = 60, max_attempts: int = 10, maximum_messages_to_keep: int = 30, should_truncate_results: bool = True, callback_handler: Any = None,) -> Agent: bedrock_model = create_bedrock_model( model=model, temperature=temperature, read_timeout=read_timeout, connect_timeout=connect_timeout, max_attempts=max_attempts, ) return Agent( system_prompt=system_prompt, model=bedrock_model, conversation_manager=SlidingWindowConversationManager( window_size=maximum_messages_to_keep, should_truncate_results=should_truncate_results, ), tools=tools, hooks=hooks, callback_handler=callback_handler, )
The xlsx_enhancer tool
This is the centerpiece. It’s a Strands @tool that orchestrates a 4-step pipeline: upload the file to the sandbox, run the inner agent, verify the output, and download the result from the sandbox.
tooldef xlsx_enhancer(input_file: str, output_file: str, instructions: str = "") -> dict: """Enhance an Excel file with professional formatting, dashboards, charts, and analysis sheets.""" input_path = Path(input_file) output_path = Path(output_file) if not input_path.exists(): return XlsxResult(success=False, error=f"Input file not found: {input_file}").model_dump() if input_path.suffix.lower() != ".xlsx": return XlsxResult(success=False, error=f"Input file must be .xlsx, got: {input_path.suffix}").model_dump() user_prompt = USER_PROMPT if instructions.strip(): user_prompt = f"{USER_PROMPT}\n\n## Additional Instructions\n{instructions}" try: code_tool = AgentCoreCodeInterpreter(region=AWS_REGION) sandbox = SandboxIO(code_tool) # 1. Upload sandbox.upload(input_path, SANDBOX_INPUT) # 2. Run the inner XLSX agent agent = create_agent( system_prompt=SYSTEM_PROMPT, model=Models.CLAUDE_46_OPUS, tools=[code_tool.code_interpreter], ) response = agent(user_prompt) # 3. Verify output exists in sandbox if not sandbox.verify_exists(SANDBOX_OUTPUT): return XlsxResult( success=False, error=f"The XLSX agent did not produce '{SANDBOX_OUTPUT}'", ).model_dump() # 4. Download output_path.parent.mkdir(parents=True, exist_ok=True) sandbox.download(SANDBOX_OUTPUT, output_path) return XlsxResult(success=True, output_path=str(output_path)).model_dump() except SandboxIOError as e: return XlsxResult(success=False, error=f"Sandbox I/O failed: {e}").model_dump()
The inner agent receives two carefully crafted prompts. The system prompt enforces hard rules about Excel integrity, formulas instead of hardcoded values, sheet name constraints, error handling. The user prompt defines the exact structure: Dashboard with KPI formulas, formatted Data sheet, executive Summary with LLM-generated insights, and Analysis sheets with charts.
The formula-first philosophy
One of the most important design decisions is that the agent never hardcodes computed values in cells. Every number in the output workbook comes from an Excel formula:
# FORBIDDEN - Computing in Pythontotal = df['Sales'].sum()sheet['B10'] = total # Hardcodes a value# REQUIRED - Excel formulassheet['B10'] = '=SUM(Data!D:D)'sheet['C10'] = '=SUMIF(Data!A:A,"Category",Data!B:B)'sheet['D10'] = '=IFERROR(AVERAGEIF(Data!A:A,A10,Data!D:D),0)'
This means the resulting Excel file is alive, change a value in the Data sheet and every KPI, every analysis table, every chart updates automatically. The IFERROR wrapping prevents #DIV/0! errors that would otherwise break AVERAGEIF formulas when a category has no data.
Handling binary files in the sandbox
The AWS Bedrock Code Interpreter sandbox runs Python in an isolated environment. Uploading the source file is straightforward, the bedrock client handles binary blobs natively. But downloading the result is trickier: the download_file method decodes everything as UTF-8, which corrupts binary xlsx files.
The solution is to base64-encode the file inside the sandbox and extract the text from the stream:
class SandboxIO: def __init__(self, code_tool: AgentCoreCodeInterpreter): self._code_tool = code_tool def _get_client(self): session_name, error = self._code_tool._ensure_session(None) if error: raise SandboxIOError(f"Failed to ensure session: {error}") session_info = self._code_tool._sessions.get(session_name) return session_info.client def upload(self, local_path: Path, sandbox_name: str = "input.xlsx") -> None: file_bytes = local_path.read_bytes() client = self._get_client() client.upload_file(path=sandbox_name, content=file_bytes) def download(self, sandbox_name: str, local_path: Path) -> None: client = self._get_client() result = client.execute_code( "import base64, os\n" f"p = '{sandbox_name}'\n" "data = open(p, 'rb').read()\n" "print(base64.b64encode(data).decode())\n" ) b64_text = _extract_stream_text(result) file_bytes = base64.b64decode(b64_text.strip()) if not file_bytes.startswith(b"PK\x03\x04"): raise SandboxIOError("Downloaded file is not a valid xlsx") local_path.write_bytes(file_bytes)
The PK\x03\x04 check validates the ZIP magic bytes — every xlsx file is a ZIP archive internally.
The original xlsx file
This is the original file we feed into the agent. It’s a flat table with rows and columns. No formatting, no formulas, just bored raw data.

What the agent produces
Given a raw financial spreadsheet, the agent generates a multi-sheet workbook:
- Dashboard: KPI cards with formulas (
=SUM(Data!D:D),=COUNT(Data!A:A)), color-coded metrics, and a hyperlinked index to all sheets

- Data: The original data with dark blue headers, alternating row colors, auto-filters, data bars on numeric columns, and frozen panes

- Summary: An executive summary written by the LLM, key findings, concentration risks, trends, anomalies, and actionable recommendations

- Analysis sheets: One per categorical column, each with a SUMIF/COUNTIF/AVERAGEIF table and a bar chart

The agent also detects the language of the input data and uses the same language for all generated content, sheet names, titles, labels, and the executive summary.
Monitoring tool execution
A simple hook tracks how long each tool execution takes. It can be extended to integrate with our application and provide real-time feedback to users about the agent’s progress:
class ToolProgressHook(HookProvider): def __init__(self) -> None: self._start_time: float = 0 def register_hooks(self, registry: HookRegistry) -> None: registry.add_callback(BeforeToolCallEvent, self.on_tool_start) registry.add_callback(AfterToolCallEvent, self.on_tool_end) def on_tool_start(self, event: BeforeToolCallEvent) -> None: self._start_time = time.time() tool_name = event.tool_use.get("name", "unknown") logger.info("Tool started: %s", tool_name) def on_tool_end(self, event: AfterToolCallEvent) -> None: elapsed = time.time() - self._start_time tool_name = event.tool_use.get("name", "unknown") logger.info("Tool finished: %s (%.1fs)", tool_name, elapsed)
And that’s all. With tools like Strands Agents and AWS Bedrock’s Code Interpreter, we can build AI agents that go beyond text generation, they produce real, functional artifacts. A raw spreadsheet goes in, a professional report comes out. No templates, no manual formatting, just an agent that understands data and knows how to present it.
Full code in my github account.






