Back to Writing Building an AI-Powered D365 F&O SSRS Report Download Agent with LangGraph

Building an AI-Powered D365 F&O SSRS Report Download Agent with LangGraph

Downloading SSRS reports from Dynamics 365 Finance & Operations (D365 F&O) traditionally requires navigating through multiple screens, remembering complex report parameters, and dealing with cumbersome interfaces. What if you could simply say "Download invoice CIV-000706 for USMF" and have an AI agent handle everything for you?

In this post, I'll show you how to build a conversational AI agent that streamlines D365 F&O SSRS report downloads using LangGraph and local AI models. This solution reduces implementation complexity by 50% while providing a natural language interface for your financial documents.

πŸ““ Complete Implementation: The full working code and step-by-step implementation is available in the complete Jupyter notebook on GitHub. Follow along with the live examples and working code demonstrations.

Contents

Why Build an AI Agent for SSRS Downloads?

Traditional SSRS report generation in D365 F&O involves multiple steps:

  1. Navigate to the appropriate module
  2. Find the correct report menu item
  3. Enter report parameters manually
  4. Wait for generation and download the PDF
  5. Repeat for each document

This process becomes tedious when you need to download multiple documents or when non-technical users need access to reports. An AI agent solves these problems by:

  • Simplifying the interface: Natural language commands instead of UI navigation
  • Automating parameter mapping: Intelligent detection of document types and required parameters
  • Batch processing: Handle multiple documents in a single request
  • Consistent file naming: Organized output with meaningful filenames

Architecture Overview

Our solution uses a clean, modular architecture built on these key components. The interactive diagram below shows how each component works together to provide a seamless document download experience.

D365 F&O SSRS Agent Architecture

User Input

Natural language commands

LangGraph Agent

AI orchestration layer

D365FO API

Integration layer

Local Gemma3

AI processing engine

PDF Files

Generated documents

User Input

Natural language commands

"Download Invoice CIV-000706 for USMF"

LangGraph Agent

AI orchestration layer

Processes commands & manages workflow

D365FO API

Integration layer

RunCopilotReport action calls

Local Gemma3

AI processing engine

Document type detection & validation

PDF Files

Generated documents

sales_invoice_CIV-000706_USMF.pdf

Data Flow Process

1
User Input Processing

Natural language command parsed and validated

2
Agent Orchestration

LangGraph coordinates workflow and tool execution

3
AI Decision Making

Gemma3 processes and validates document parameters

4
D365FO Integration

API calls to RunCopilotReport with validated parameters

5
Document Generation

PDF files created with organized naming conventions

↻
Feedback Loop

Continuous learning and workflow optimization

The agent processes natural language input, determines the correct D365 F&O parameters, makes the API call, and saves the resulting PDF with an organized filename. Each component plays a crucial role:

  • User Input: Natural language commands like "Download invoice CIV-000706 for USMF"
  • LangGraph Agent: Orchestrates the entire workflow and manages component interactions
  • Local Gemma3: Provides AI processing for document type detection and validation
  • D365FO API: Handles integration with D365 F&O via RunCopilotReport actions
  • PDF Output: Generated documents with organized naming conventions

Key Features and Benefits

βœ… Single Unified Tool

  • One function handles both D365FO action calls and PDF saving
  • Auto-detects document types from user input
  • Comprehensive error handling and validation

βœ… Clean Architecture

  • 50% less code compared to traditional implementations
  • Clear separation of concerns
  • Well-documented functions with type hints
  • Robust error handling

βœ… Privacy and Performance

  • πŸ”’ Privacy: All AI processing happens locally with Gemma3
  • πŸš€ Performance: No API rate limits or external dependencies
  • πŸ’° Cost-Effective: No per-request charges for AI inference
  • πŸ”§ Customizable: Full control over model behavior and responses
  • πŸ“± Offline Ready: Works without internet connectivity

Prerequisites and Setup

Before implementing the solution, ensure you have:

  1. LMStudio installed with Gemma3 model loaded
  2. D365FO credentials configured in environment variables
  3. Required Python packages installed

Environment Configuration

Set these environment variables:

  • D365FO_TENANT_ID - Your Azure AD tenant ID
  • D365FO_CLIENT_ID - Your Azure AD application client ID
  • D365FO_CLIENT_SECRET - Your Azure AD application client secret
# Environment setup with secure input handling
env_keys = {
    'D365FO_TENANT_ID': 'Enter your D365FO Tenant ID',
    'D365FO_CLIENT_ID': 'Enter your D365FO Client ID',
    'D365FO_CLIENT_SECRET': 'Enter your D365FO Client Secret'
}

for key, prompt in env_keys.items():
    if key not in os.environ:
        value = getpass(f"{prompt}: ")
        os.environ[key] = value

D365FO Client Setup

from d365fo_client import FOClient, FOClientConfig
from d365fo_client.credential_sources import EnvironmentCredentialSource

# Configure client with your D365FO environment
config = FOClientConfig(
    base_url="https://your-d365fo-environment.dynamics.com/",
    credential_source=EnvironmentCredentialSource(),
    verify_ssl=True,  # Set to False for development environments
)

client = FOClient(config)

Core Implementation

Document Type Mappings

The foundation of our agent is a clean mapping system that translates document types to their corresponding D365 F&O controllers and parameters:

from enum import Enum

class DocumentType(str, Enum):
    SALES_INVOICE = "sales_invoice"
    FREE_TEXT_INVOICE = "free_text_invoice"
    SALES_CONFIRMATION = "sales_confirmation"
    PURCHASE_CONFIRMATION = "purchase_confirmation"

# Document type mappings to D365FO controllers
DOCUMENT_MAPPINGS = {
    'sales_invoice': {
        'controller': 'SalesInvoiceController',
        'table': 'CustInvoiceJour',
        'field': 'InvoiceId'
    },
    'free_text_invoice': {
        'controller': 'FreeTextInvoiceController',
        'table': 'CustInvoiceJour',
        'field': 'InvoiceId',
    },
    'sales_confirmation': {
        'controller': 'SalesConfirmController',
        'table': 'CustConfirmJour',
        'field': 'ConfirmId',
    },
    'purchase_confirmation': {
        'controller': 'PurchPurchaseOrderController',
        'table': 'VendPurchOrderJour',
        'field': 'PurchId'
    }
}

Unified Download Function

The core of our implementation is a single async function that handles the entire workflow:

async def download_d365fo_report(
    document_id: str,
    legal_entity: str,
    document_type: Optional[DocumentType] = DocumentType.SALES_INVOICE,
    output_directory: str = "./Reports"
) -> Dict[str, Any]:
    """
    Download a D365FO report and save it as a PDF file.

    This function handles the complete workflow:
    1. Detects document type if not provided
    2. Calls the D365FO RunCopilotReport action
    3. Extracts the base64 PDF from the response
    4. Saves the PDF file with a meaningful name
    """
    try:
        # Get document mapping
        if document_type not in DOCUMENT_MAPPINGS:
            return {
                "success": False,
                "message": f"Unsupported document type: {document_type}",
                "file_path": None
            }

        mapping = DOCUMENT_MAPPINGS[document_type]

        # Prepare controller arguments
        controller_args = {
            "DataTableName": mapping['table'],
            "DataTableFieldName": mapping['field'],
            "DataTableFieldValue": document_id
        }

        # Prepare action parameters
        parameters = {
            "_contractName": "SrsCopilotArgsContract",
            "_controllerArgsJson": json.dumps(controller_args),
            "_controllerName": mapping['controller'],
            "_legalEntityName": legal_entity,
            "_reportParameterJson": "{}"
        }

        # Call D365FO action
        result = await client.call_action(
            action_name="RunCopilotReport",
            parameters=parameters,
            entity_name="SrsFinanceCopilots",
            skip_validation=True
        )

        # Extract and save PDF
        base64_string = result.get('value')
        if not base64_string:
            return {
                "success": False,
                "message": "No PDF data found in D365FO response",
                "file_path": None
            }

        # Decode and save PDF
        pdf_binary = base64.b64decode(base64_string)
        Path(output_directory).mkdir(parents=True, exist_ok=True)

        filename = f"{document_type.value}_{document_id}_{legal_entity}.pdf"
        file_path = Path(output_directory) / filename

        with open(file_path, 'wb') as pdf_file:
            pdf_file.write(pdf_binary)

        file_size_kb = len(pdf_binary) / 1024

        return {
            "success": True,
            "message": f"Successfully downloaded {document_type.value} {document_id}",
            "file_path": str(file_path.absolute()),
            "file_size_kb": round(file_size_kb, 1),
            "document_type": document_type.value,
            "filename": filename
        }

    except Exception as e:
        return {
            "success": False,
            "message": f"Error: {str(e)}",
            "file_path": None
        }

LangGraph Agent Configuration

The agent uses a clean system prompt and integrates seamlessly with local AI models:

from langchain_openai import ChatOpenAI
from langchain_core.tools import StructuredTool
from langchain.agents import create_agent

# System prompt for the agent
SYSTEM_PROMPT = """
You are a D365FO document download assistant. Your job is to download financial documents (invoices, confirmations, purchase orders) as PDF files.

## Supported Documents
- **Sales Invoice**: Use `SalesInvoiceController` with `CustInvoiceJour` table and `InvoiceId` field
- **Free Text Invoice**: Use `FreeTextInvoiceController` with `CustInvoiceJour` table and `InvoiceId` field
- **Sales Confirmation**: Use `SalesConfirmController` with `CustConfirmJour` table and `ConfirmId` field
- **Purchase Confirmation**: Use `PurchPurchaseOrderController` with `VendPurchOrderJour` table and `PurchId` field

## Process
1. Extract document type, ID, and legal entity from user request
2. Use the `download_d365fo_report` tool with appropriate parameters
3. The tool handles both the D365FO action call and PDF saving automatically

Keep responses concise and confirm successful downloads with file details.
"""

# Create the tool
d365fo_download_tool = StructuredTool.from_function(
    name="download_d365fo_report",
    description="Download financial documents from D365FO as PDF files",
    coroutine=download_d365fo_report,
    args_schema=D365FOReportInput,
)

# Create LLM connected to local Gemma3
llm = ChatOpenAI(
    model="google/gemma-3n-e4b",  # Your loaded model name
    base_url="http://localhost:1234/v1",  # LMStudio endpoint
    api_key=SecretStr("not-needed"),
    temperature=0
)

# Create agent
agent = create_agent(
    llm,
    tools=[d365fo_download_tool],
    system_prompt=SYSTEM_PROMPT
)

Supported Document Types

Our implementation supports the most commonly used D365 F&O financial documents:

Document Type Controller Table Field Example ID
Sales Invoice SalesInvoiceController CustInvoiceJour InvoiceId CIV-000706
Free Text Invoice FreeTextInvoiceController CustInvoiceJour InvoiceId FTI-00000002
Sales Confirmation SalesConfirmController CustConfirmJour ConfirmId CCF-00000060
Purchase Order PurchPurchaseOrderController VendPurchOrderJour PurchId PO-456

The agent automatically detects document types based on ID patterns and context from the user's request.

Practical Examples

Single Document Download

# Download a sales invoice
response = await agent.ainvoke({
    "messages": "Download sales invoice CIV-000205 for USMF"
})

# Result: sales_invoice_CIV-000205_USMF.pdf (194.9 KB)

Multiple Documents

# Download multiple invoices in one request
response = await agent.ainvoke({
    "messages": "Download sales invoice CIV-000205 and CIV-000234 for USMF"
})

# Results:
# - sales_invoice_CIV-000205_USMF.pdf (194.9 KB)
# - sales_invoice_CIV-000234_USMF.pdf (194.9 KB)

Different Document Types

# Free text invoices
response = await agent.ainvoke({
    "messages": "Download customer free text invoice FTI-00000002 FTI-00000003 for USMF"
})

# Sales confirmations
response = await agent.ainvoke({
    "messages": "Download sales order confirmation CCF-00000060 and CCF-00000059 for USMF"
})

Error Handling and Validation

The implementation includes comprehensive error handling:

Input Validation

from pydantic import BaseModel, Field

class D365FOReportInput(BaseModel):
    """Input schema for D365FO report download tool"""
    document_id: str = Field(description="Document identifier")
    legal_entity: str = Field(description="Legal entity code (e.g., USMF, DEMF)")
    document_type: Optional[DocumentType] = Field(
        default=DocumentType.SALES_INVOICE,
        description="Document type - auto-detected if not provided"
    )
    output_directory: str = Field(default="./Reports", description="PDF save directory")

Exception Handling

try:
    # D365FO API call
    result = await client.call_action(...)

except FOClientError as e:
    return {
        "success": False,
        "message": f"D365FO client error: {str(e)}",
        "file_path": None
    }
except Exception as e:
    return {
        "success": False,
        "message": f"Unexpected error: {str(e)}",
        "file_path": None
    }

Response Validation

# Validate D365FO response
if not result.get('value'):
    return {
        "success": False,
        "message": f"D365FO action failed: {result.get('message', 'Unknown error')}",
        "file_path": None
    }

# Validate PDF data
base64_string = result.get('value')
if not base64_string:
    return {
        "success": False,
        "message": "No PDF data found in D365FO response",
        "file_path": None
    }

Performance and Privacy Benefits

Local AI Processing

Using Gemma3 through LMStudio provides several advantages:

  • Privacy: No data sent to external AI services
  • Performance: No network latency for AI inference
  • Cost: No per-request charges
  • Availability: Works offline
  • Customization: Full control over model behavior

Optimized Implementation

The streamlined architecture delivers:

  • 50% code reduction compared to traditional implementations
  • Faster development with clear separation of concerns
  • Easier maintenance with well-documented functions
  • Better error handling with comprehensive validation

Resource Management

# Proper cleanup
try:
    # Agent operations
    response = await agent.ainvoke({"messages": user_input})
finally:
    # Clean up D365FO client connection
    await client.close()

Next Steps and Extensions

1. Batch Processing Enhancement

Extend the implementation to handle larger document sets:

async def download_document_batch(
    document_list: List[Dict[str, str]],
    output_directory: str = "./Reports"
) -> List[Dict[str, Any]]:
    """Download multiple documents in parallel"""
    tasks = [
        download_d365fo_report(
            document_id=doc['id'],
            legal_entity=doc['entity'],
            document_type=doc.get('type'),
            output_directory=output_directory
        )
        for doc in document_list
    ]
    return await asyncio.gather(*tasks)

2. Web Interface Integration

Build a REST API or web interface around the core functionality:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

class DownloadRequest(BaseModel):
    query: str  # Natural language request
    output_directory: str = "./Reports"

@app.post("/download")
async def download_reports(request: DownloadRequest):
    """REST endpoint for report downloads"""
    try:
        response = await agent.ainvoke({"messages": request.query})
        return {"success": True, "response": response}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

3. Advanced Document Type Detection

Implement machine learning-based document type detection:

def detect_document_type(document_id: str, context: str) -> DocumentType:
    """Enhanced document type detection using patterns and context"""
    # Pattern-based detection
    patterns = {
        r'^CIV-': DocumentType.SALES_INVOICE,
        r'^FTI-': DocumentType.FREE_TEXT_INVOICE,
        r'^CCF-': DocumentType.SALES_CONFIRMATION,
        r'^PO-': DocumentType.PURCHASE_CONFIRMATION,
    }

    for pattern, doc_type in patterns.items():
        if re.match(pattern, document_id):
            return doc_type

    # Context-based detection using AI
    # Implementation depends on your specific needs
    return DocumentType.SALES_INVOICE  # Default

4. Monitoring and Logging

Add comprehensive monitoring for production use:

import structlog
from datetime import datetime

logger = structlog.get_logger()

async def download_with_monitoring(
    document_id: str,
    legal_entity: str,
    **kwargs
) -> Dict[str, Any]:
    """Download with comprehensive logging"""
    start_time = datetime.now()

    logger.info(
        "download_started",
        document_id=document_id,
        legal_entity=legal_entity,
        timestamp=start_time.isoformat()
    )

    try:
        result = await download_d365fo_report(
            document_id, legal_entity, **kwargs
        )

        duration = (datetime.now() - start_time).total_seconds()

        logger.info(
            "download_completed",
            document_id=document_id,
            success=result['success'],
            duration_seconds=duration,
            file_size_kb=result.get('file_size_kb')
        )

        return result

    except Exception as e:
        logger.error(
            "download_failed",
            document_id=document_id,
            error=str(e),
            duration_seconds=(datetime.now() - start_time).total_seconds()
        )
        raise

Get in Touch

Need help implementing AI-powered automation in your D365 F&O environment? Want to discuss custom integration solutions or enterprise AI consulting?

Connect with me:

Whether you're looking for consulting services, training, or just want to discuss D365 F&O automation strategies, I'd love to hear from you!

Share this article