Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions answer_rocket/graphql/operations/layouts.gql
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,13 @@ query getDynamicLayout($id: UUID!) {
name
}
}
}

mutation GeneratePdfFromLayouts($layouts: [String!]!) {
generatePdfFromLayouts(layouts: $layouts) {
success
code
error
pdf
}
}
15 changes: 14 additions & 1 deletion answer_rocket/graphql/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,15 @@ class HydratedReport(sgqlc.types.Type):
meta = sgqlc.types.Field(JSON, graphql_name='meta')


class LayoutPdfResponse(sgqlc.types.Type):
__schema__ = schema
__field_names__ = ('success', 'code', 'error', 'pdf')
success = sgqlc.types.Field(sgqlc.types.non_null(Boolean), graphql_name='success')
code = sgqlc.types.Field(String, graphql_name='code')
error = sgqlc.types.Field(String, graphql_name='error')
pdf = sgqlc.types.Field(String, graphql_name='pdf')


class MatchValues(sgqlc.types.Type):
__schema__ = schema
__field_names__ = ('automatic_db_whitelist', 'constrained_values', 'dataset_id', 'default_performance_metric', 'inverse_map', 'phrase_template', 'popular_values', 'value_collection_name', 'dataset_date_dimensions', 'dataset_dimensions', 'dataset_metrics', 'predicate_vocab', 'defer_predicate_vocab_loading')
Expand Down Expand Up @@ -1235,7 +1244,7 @@ class ModelOverrideResult(sgqlc.types.Type):

class Mutation(sgqlc.types.Type):
__schema__ = schema
__field_names__ = ('create_max_copilot_skill_chat_question', 'update_max_copilot_skill_chat_question', 'delete_max_copilot_skill_chat_question', 'create_max_copilot_question', 'update_max_copilot_question', 'delete_max_copilot_question', 'run_copilot_skill_async', 'clear_copilot_cache', 'create_copilot_test_run', 'run_copilot_test_run', 'cancel_copilot_test_run', 'set_max_agent_workflow', 'import_copilot_skill_from_zip', 'sync_max_skill_repository', 'import_skill_from_repo', 'test_run_copilot_skill', 'get_test_run_output', 'reload_dataset', 'update_database_name', 'update_database_description', 'update_database_llm_description', 'update_database_mermaid_er_diagram', 'update_database_kshot_limit', 'update_dataset_name', 'update_dataset_description', 'update_dataset_date_range', 'update_dataset_data_interval', 'update_dataset_misc_info', 'update_dataset_source', 'update_dataset_query_row_limit', 'update_dataset_use_database_casing', 'update_dataset_kshot_limit', 'create_dataset', 'create_dataset_from_table', 'create_dimension', 'update_dimension', 'delete_dimension', 'create_metric', 'update_metric', 'delete_metric', 'create_database_kshot', 'update_database_kshot_question', 'update_database_kshot_rendered_prompt', 'update_database_kshot_explanation', 'update_database_kshot_sql', 'update_database_kshot_title', 'update_database_kshot_visualization', 'delete_database_kshot', 'create_dataset_kshot', 'update_dataset_kshot_question', 'update_dataset_kshot_rendered_prompt', 'update_dataset_kshot_explanation', 'update_dataset_kshot_sql', 'update_dataset_kshot_title', 'update_dataset_kshot_visualization', 'delete_dataset_kshot', 'update_chat_answer_payload', 'ask_chat_question', 'evaluate_chat_question', 'queue_chat_question', 'cancel_chat_question', 'create_chat_thread', 'add_feedback', 'set_skill_memory', 'share_thread', 'update_loading_message', 'create_chat_artifact', 'delete_chat_artifact', 'send_email')
__field_names__ = ('create_max_copilot_skill_chat_question', 'update_max_copilot_skill_chat_question', 'delete_max_copilot_skill_chat_question', 'create_max_copilot_question', 'update_max_copilot_question', 'delete_max_copilot_question', 'run_copilot_skill_async', 'clear_copilot_cache', 'create_copilot_test_run', 'run_copilot_test_run', 'cancel_copilot_test_run', 'set_max_agent_workflow', 'import_copilot_skill_from_zip', 'sync_max_skill_repository', 'import_skill_from_repo', 'test_run_copilot_skill', 'get_test_run_output', 'reload_dataset', 'update_database_name', 'update_database_description', 'update_database_llm_description', 'update_database_mermaid_er_diagram', 'update_database_kshot_limit', 'update_dataset_name', 'update_dataset_description', 'update_dataset_date_range', 'update_dataset_data_interval', 'update_dataset_misc_info', 'update_dataset_source', 'update_dataset_query_row_limit', 'update_dataset_use_database_casing', 'update_dataset_kshot_limit', 'create_dataset', 'create_dataset_from_table', 'create_dimension', 'update_dimension', 'delete_dimension', 'create_metric', 'update_metric', 'delete_metric', 'create_database_kshot', 'update_database_kshot_question', 'update_database_kshot_rendered_prompt', 'update_database_kshot_explanation', 'update_database_kshot_sql', 'update_database_kshot_title', 'update_database_kshot_visualization', 'delete_database_kshot', 'create_dataset_kshot', 'update_dataset_kshot_question', 'update_dataset_kshot_rendered_prompt', 'update_dataset_kshot_explanation', 'update_dataset_kshot_sql', 'update_dataset_kshot_title', 'update_dataset_kshot_visualization', 'delete_dataset_kshot', 'update_chat_answer_payload', 'ask_chat_question', 'evaluate_chat_question', 'queue_chat_question', 'cancel_chat_question', 'create_chat_thread', 'add_feedback', 'set_skill_memory', 'share_thread', 'update_loading_message', 'create_chat_artifact', 'delete_chat_artifact', 'generate_pdf_from_layouts', 'send_email')
create_max_copilot_skill_chat_question = sgqlc.types.Field(sgqlc.types.non_null(CreateMaxCopilotSkillChatQuestionResponse), graphql_name='createMaxCopilotSkillChatQuestion', args=sgqlc.types.ArgDict((
('copilot_id', sgqlc.types.Arg(sgqlc.types.non_null(UUID), graphql_name='copilotId', default=None)),
('copilot_skill_id', sgqlc.types.Arg(sgqlc.types.non_null(UUID), graphql_name='copilotSkillId', default=None)),
Expand Down Expand Up @@ -1603,6 +1612,10 @@ class Mutation(sgqlc.types.Type):
)
delete_chat_artifact = sgqlc.types.Field(sgqlc.types.non_null(MaxMutationResponse), graphql_name='deleteChatArtifact', args=sgqlc.types.ArgDict((
('chat_artifact_id', sgqlc.types.Arg(sgqlc.types.non_null(UUID), graphql_name='chatArtifactId', default=None)),
))
)
generate_pdf_from_layouts = sgqlc.types.Field(sgqlc.types.non_null(LayoutPdfResponse), graphql_name='generatePdfFromLayouts', args=sgqlc.types.ArgDict((
('layouts', sgqlc.types.Arg(sgqlc.types.non_null(sgqlc.types.list_of(sgqlc.types.non_null(String))), graphql_name='layouts', default=None)),
))
)
send_email = sgqlc.types.Field(sgqlc.types.non_null(EmailSendResponse), graphql_name='sendEmail', args=sgqlc.types.ArgDict((
Expand Down
11 changes: 11 additions & 0 deletions answer_rocket/graphql/sdk_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,16 @@ def mutation_send_email():
return _op


def mutation_generate_pdf_from_layouts():
_op = sgqlc.operation.Operation(_schema_root.mutation_type, name='GeneratePdfFromLayouts', variables=dict(layouts=sgqlc.types.Arg(sgqlc.types.non_null(sgqlc.types.list_of(sgqlc.types.non_null(_schema.String))))))
_op_generate_pdf_from_layouts = _op.generate_pdf_from_layouts(layouts=sgqlc.types.Variable('layouts'))
_op_generate_pdf_from_layouts.success()
_op_generate_pdf_from_layouts.code()
_op_generate_pdf_from_layouts.error()
_op_generate_pdf_from_layouts.pdf()
return _op


def mutation_update_loading_message():
_op = sgqlc.operation.Operation(_schema_root.mutation_type, name='UpdateLoadingMessage', variables=dict(answerId=sgqlc.types.Arg(sgqlc.types.non_null(_schema.UUID)), message=sgqlc.types.Arg(sgqlc.types.non_null(_schema.String))))
_op.update_loading_message(answer_id=sgqlc.types.Variable('answerId'), message=sgqlc.types.Variable('message'))
Expand Down Expand Up @@ -568,6 +578,7 @@ class Mutation:
delete_dataset_kshot = mutation_delete_dataset_kshot()
delete_dimension = mutation_delete_dimension()
delete_metric = mutation_delete_metric()
generate_pdf_from_layouts = mutation_generate_pdf_from_layouts()
get_test_run_output = mutation_get_test_run_output()
import_copilot_skill_from_zip = mutation_import_copilot_skill_from_zip()
import_skill_from_repo = mutation_import_skill_from_repo()
Expand Down
91 changes: 90 additions & 1 deletion answer_rocket/layouts.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
from __future__ import annotations

from typing import Optional, Dict, Any, List
from uuid import UUID

from answer_rocket.client_config import ClientConfig
from answer_rocket.graphql.client import GraphQlClient
from answer_rocket.graphql.sdk_operations import Operations
Expand Down Expand Up @@ -31,6 +36,90 @@ def get_dynamic_layout(self, id: str):
})

return result.get_dynamic_layout

except Exception as e:
raise Exception(f"Failed to get dynamic layout: {e}")

def generate_pdf_from_layouts(
self,
layouts: List[str]
):
"""
Generate a PDF from a list of layout JSON strings.

This is the primary method for SDK users to generate PDFs from layouts.
Uses the same rendering infrastructure as the "Download PDF" button in the UI,
ensuring identical output. Returns base64-encoded PDF data that can be
directly passed to sendEmail as an attachment.

Parameters
----------
layouts : List[str]
List of layout JSON strings to render as PDF pages.
Each string should be a valid layout JSON representation.

Returns
-------
LayoutPdfResponse
Response object containing:
- success: bool - Whether the PDF generation completed successfully
- code: str - Optional status or error code
- error: str - Human-readable error message if the operation failed
- pdf: str - Base64-encoded PDF data, ready to use as an email attachment payload

Examples
--------
Generate a PDF from a single layout:

>>> # First, get the layout
>>> layout = max.dynamic_layouts.get_dynamic_layout(
... id="12345678-1234-1234-1234-123456789abc"
... )
>>> # Generate PDF from the layout JSON
>>> result = max.dynamic_layouts.generate_pdf_from_layouts(
... layouts=[layout.layout_json]
... )
>>> if result.success:
... import base64
... pdf_bytes = base64.b64decode(result.pdf)
... with open('output.pdf', 'wb') as f:
... f.write(pdf_bytes)

Generate a multi-page PDF from multiple layouts:

>>> layout1 = max.dynamic_layouts.get_dynamic_layout(id="layout-1-id")
>>> layout2 = max.dynamic_layouts.get_dynamic_layout(id="layout-2-id")
>>> result = max.dynamic_layouts.generate_pdf_from_layouts(
... layouts=[layout1.layout_json, layout2.layout_json]
... )
>>> if result.success:
... print(f"Generated multi-page PDF")
... else:
... print(f"Error: {result.error}")

Use the PDF directly as an email attachment:

>>> layout = max.dynamic_layouts.get_dynamic_layout(id="layout-id")
>>> result = max.dynamic_layouts.generate_pdf_from_layouts(
... layouts=[layout.layout_json]
... )
>>> if result.success:
... email_result = max.email.send_email(
... user_ids=[uuid.UUID("user-id-here")],
... subject="Your Report",
... body="Please see the attached PDF report.",
... attachments=[{
... 'filename': 'report.pdf',
... 'payload': result.pdf,
... 'type': 'application/pdf'
... }]
... )
"""
mutation_args = {
'layouts': layouts,
}

op = Operations.mutation.generate_pdf_from_layouts
result = self._gql_client.submit(op, mutation_args)

return result.generate_pdf_from_layouts