# Retrieve all jobs get /v1/applications/{application_id}/jobs Retrieve all jobs created via the async API, linked to the provided application ID (or alias). # Generate from application post /v1/applications/{application_id} Generate content from an existing no-code application with inputs. # Generate from application (async) post /v1/applications/{application_id}/jobs Generate content asynchronously from an existing application with inputs. # Retrieve a single job get /v1/applications/jobs/{job_id} Retrieves a single job created via the Async api # Retry job execution post /v1/applications/jobs/{job_id}/retry Re-triggers the async execution of a single job previously created via the Async api and terminated in error. # Chat completion post /v1/chat Generate a chat completion based on the provided messages. The response shown below is for non-streaming. To learn about streaming responses, see the [chat completion guide](/api-guides/chat-completion). # List models get /v1/models # Text generation post /v1/completions # Delete file delete /v1/files/{file_id} # Download file get /v1/files/{file_id}/download # Retry failed files post /v1/files/retry # List files get /v1/files # Retrieve file get /v1/files/{file_id} # Upload file post /v1/files # Add file to graph post /v1/graphs/{graph_id}/file # Create graph post /v1/graphs # Delete graph delete /v1/graphs/{graph_id} # List graphs get /v1/graphs # Question post /v1/graphs/question Ask a question to specified Knowledge Graphs. # Remove file from graph delete /v1/graphs/{graph_id}/file/{file_id} # Retrieve graph get /v1/graphs/{graph_id} # Update graph put /v1/graphs/{graph_id} # Medical comprehend post /v1/tools/comprehend/medical Analyze unstructured medical text to extract entities labeled with standardized medical codes and confidence scores. # Context-aware text splitting post /v1/tools/context-aware-splitting Splits a long block of text (maximum 4000 words) into smaller chunks while preserving the semantic meaning of the text and context between the chunks. # Parse PDF post /v1/tools/pdf-parser/{file_id} Parse PDF to other formats. # Graph file upload tool This tutorial will guide you through creating a Python script that uploads a specified directory of files to a Writer Knowledge Graph using the Writer Python SDK. You'll use Poetry for dependency management and environment setup. ## Prerequisites * Python 3.8 or higher installed * [Poetry](https://pypi.org/project/poetry/) installed ([Installation guide](https://python-poetry.org/docs/#installation)) * A Writer API key (follow the [Quickstart](/api-guides/quickstart) to create an app and obtain an API key) * A Writer Knowledge Graph ID You can obtain a **Knowledge Graph ID** by either creating a new graph using the [Create Graph endpoint](/kg-api/create-graph) or using the [List Graphs endpoint](/kg-api/list-graphs) to retrieve an existing Graph's ID. ## Set up the project Create a new directory for your project: ``` mkdir writer-file-uploader cd writer-file-uploader ``` Initialize a new Poetry project: ``` poetry init ``` Follow the prompts, accepting the defaults for most options. Add the necessary dependencies to your project: ``` poetry add writer-sdk python-dotenv ``` ## Set up environment variables Create a `.env` file in the project root: ``` touch .env ``` Add your Writer API key and graph ID to the `.env` file: ``` WRITER_API_KEY=your_api_key_here GRAPH_ID=your_graph_id_here ``` ## Create the main script Create a file named `main.py` in the project root and add the following code: ```python import os import sys from dotenv import load_dotenv from writer import Writer # Load environment variables from .env file load_dotenv() def main(directory_path: str) -> None: # Check if the provided path is a valid directory if not os.path.isdir(directory_path): print(f"Error: {directory_path} is not a valid directory") sys.exit(1) # Initialize the Writer client client = Writer() # Get the graph ID from environment variables graph_id = os.getenv("GRAPH_ID") if not graph_id: print("Error: GRAPH_ID environment variable is not set") sys.exit(1) print(f"Using graph: {graph_id}") if __name__ == "__main__": # Ensure the script is called with a directory path argument if len(sys.argv) != 2: print("Usage: python main.py ") sys.exit(1) directory_path = sys.argv[1] main(directory_path) ``` In this initial setup, you're importing the necessary modules for your script. The `os` module will be used for file operations, `sys` for system-level operations, `load_dotenv` to load environment variables, and `Writer` from the Writer SDK to interact with the Writer API. The `load_dotenv()` function is crucial as it loads environment variables from a `.env` file. This allows you to keep sensitive information like API keys separate from your code, enhancing security and flexibility. In the `main` function, you first check if the provided path is a valid directory. This is an important validation step to ensure the script doesn't proceed with an invalid input. If the path is invalid, the script exits with an error message. You then initialize the Writer client, which you'll use throughout the script to interact with the Writer API. The graph ID is retrieved from environment variables. If the `GRAPH_ID` isn't set, the script exits with an error, ensuring all necessary information is available before proceeding. The `if __name__ == "__main__":` block at the end ensures that the `main` function is only called if the script is run directly, not if it's imported as a module. Add the following function to list files in the specified directory: ```python from typing import List def get_files_in_directory(directory_path: str) -> List[str]: return [ os.path.join(directory_path, f) for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f)) ] # Update the main function def main(directory_path: str) -> None: # ... (previous code) files = get_files_in_directory(directory_path) print(f"Found {len(files)} files in the directory.") ``` The `get_files_in_directory` function uses a list comprehension to create a list of full file paths for all files (excluding directories) in the specified directory. This function leverages several `os` module methods to ensure cross-platform compatibility. You use `os.path.join` to create full file paths, which is important for ensuring your script works across different operating systems. The `os.listdir` function lists all entries in the directory, while `os.path.isfile` checks if each entry is a file rather than a subdirectory. In the `main` function, you call this new function and print the number of files found. This provides immediate feedback to the user about what the script will process, which is a good practice for user-friendly command-line tools. Add a function to upload a single file: ```python def upload_file(file_path: str, client: Writer) -> str: # Open and read the file contents with open(file_path, 'rb') as file_obj: file_contents = file_obj.read() # Upload the file using the Writer SDK file = client.files.upload( content=file_contents, content_disposition=f"attachment; filename={os.path.basename(file_path)}", content_type="application/octet-stream", ) return file.id ``` The `upload_file` function is where you interact with the Writer API to upload a file. This function takes two parameters: the path to the file you want to upload, and the Writer client you initialized earlier. You start by opening the file in binary mode ('rb') to read its contents. This ensures that the file is read correctly regardless of its type (text, image, etc.). Next, you use the Writer SDK's `upload` method to send the file to Writer. The `content_disposition` parameter tells the server to treat this as an attachment and specifies the filename. You're using `application/octet-stream` as a generic content type, which is a safe choice that works for any file type. The function returns the ID of the uploaded file. This ID is crucial as you'll use it later to associate the file with a specific graph. Add a function to associate a file with a graph: ```python def associate_file_with_graph(file_id: str, graph_id: str, client: Writer) -> None: client.graphs.add_file_to_graph( graph_id, file_id=file_id, ) ``` The `associate_file_with_graph` function takes three parameters: the ID of the file you just uploaded, the ID of the graph you want to associate it with, and the Writer client. This function uses the Writer SDK's `add_file_to_graph` method to create the association between the file and the graph. This step is essential for making the uploaded file available within the context of a specific Writer graph. Create a function that combines file upload and graph association: ```python def upload_and_associate_file(file_path: str, client: Writer, graph_id: str) -> str: file_id = upload_file(file_path, client) associate_file_with_graph(file_id, graph_id, client) return f"Processed {file_path}: File ID {file_id} added to graph {graph_id}" ``` The `upload_and_associate_file` function is a higher-level function that combines the upload and association steps into a single operation. This encapsulation simplifies the main logic of your script and makes it easier to handle each file as a unit. In this function, you first call `upload_file` to upload the file and get its ID. Then, you immediately call `associate_file_with_graph` to link this file with the specified graph. This ensures that each file is associated with the graph as soon as it's uploaded. The function returns a string describing the action taken. This return value is useful for providing detailed feedback to the user, allowing them to track the progress of each file. Update the `main` function to process files one by one: ```python def main(directory_path: str) -> None: # ... (previous code) files = get_files_in_directory(directory_path) for file in files: result = upload_and_associate_file(file, client, graph_id) print(result) print("All files have been processed.") ``` This implementation processes files sequentially, one at a time. It's a straightforward approach that's easy to understand and debug. You iterate through each file in the list, call `upload_and_associate_file`, and immediately print the result. While this method is simpler, it may be slower for a large number of files because it processes them one after another. However, it provides real-time feedback to the user as each file is processed, which can be beneficial for tracking progress. Finally, update the `main` function to use `ThreadPoolExecutor` for parallel processing: ```python from concurrent.futures import ThreadPoolExecutor, as_completed def main(directory_path: str) -> None: # ... (previous code) files = get_files_in_directory(directory_path) with ThreadPoolExecutor() as executor: future_to_file = {executor.submit(upload_and_associate_file, file, client, graph_id): file for file in files} for future in as_completed(future_to_file): print(future.result()) print("All files have been processed.") ``` This final version of the `main` function introduces parallel processing using Python's `ThreadPoolExecutor`. This approach can significantly speed up the overall process, especially when dealing with many files or slow network connections. You create a "future" for each file processing task. A future represents a computation that may or may not have completed yet. The `executor.submit` method starts each task in a separate thread. The `as_completed` function yields futures as they complete. This allows you to print results as soon as they're available, rather than waiting for all files to be processed. This approach provides a good balance between efficiency and user feedback. As a final touch, add graph status reporting at the beginning and end: ```python def main(directory_path: str) -> None: # ... (previous code) graph = client.graphs.retrieve(graph_id=graph_id) print(f"Initial files in graph: {graph.file_status}") # ... (processing files) graph = client.graphs.retrieve(graph_id=graph_id) print(f"Final files in graph: {graph.file_status}") ``` This final addition provides a before-and-after snapshot of the graph's file status. By retrieving and displaying the graph status at the beginning and end of the process, you give the user a clear picture of the script's impact. The initial status shows the state of the graph before any operations, while the final status confirms that all files were successfully associated with the graph. This kind of reporting is valuable for verifying the script's effectiveness and can be crucial for debugging or auditing purposes. ## Verify the final script Here is the final `main.py` file: ```python import os import sys from concurrent.futures import ThreadPoolExecutor, as_completed from typing import List from dotenv import load_dotenv from writerai import Writer # Load environment variables load_dotenv() def upload_and_associate_file(file_path: str, client: Writer, graph_id: str) -> str: with open(file_path, 'rb') as file_obj: file_contents = file_obj.read() file = client.files.upload( content=file_contents, content_disposition=f"attachment; filename={os.path.basename(file_path)}", content_type="application/octet-stream", ) client.graphs.add_file_to_graph( graph_id, file_id=file.id, ) return f"Processed {file_path}: File ID {file.id} added to graph {graph_id}" def get_files_in_directory(directory_path: str) -> List[str]: return [ os.path.join(directory_path, f) for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f)) ] def main(directory_path: str) -> None: if not os.path.isdir(directory_path): print(f"Error: {directory_path} is not a valid directory") sys.exit(1) client = Writer() graph_id = os.getenv("GRAPH_ID") if not graph_id: print("Error: GRAPH_ID environment variable is not set") sys.exit(1) print(f"Using graph: {graph_id}") graph = client.graphs.retrieve(graph_id=graph_id) print(f"Files in graph: {graph.file_status}") files = get_files_in_directory(directory_path) with ThreadPoolExecutor() as executor: future_to_file = {executor.submit(upload_and_associate_file, file, client, graph_id): file for file in files} for future in as_completed(future_to_file): print(future.result()) print("All files have been processed.") # List files for graph graph = client.graphs.retrieve(graph_id=graph_id) print(f"Files in graph: {graph.file_status}") if __name__ == "__main__": if len(sys.argv) != 2: print("Usage: python src/main.py ") sys.exit(1) directory_path = sys.argv[1] main(directory_path) ``` ## Run the script Activate the Poetry shell: ``` poetry shell ``` Run the script, providing a directory path as an argument: ``` python main.py /path/to/your/directory ``` The script will upload all files in the specified directory to your Knowledge Graph and associate them with it. ## Conclusion You've now created a Python script that uses the Writer Python SDK to upload a directory of files to a Knowledge Graph. You can further customize this script to handle different file types, add error handling, or integrate it into larger applications. # No-code applications This guide explains the [Applications endpoint](../api-guides/api-reference/completion-api/applications), which generates content from deployed no-code applications with inputs. This guide will help you understand how to effectively interact with the Applications API. The Applications API allows you to turn deployed no-code applications into microservices. Business users can define inputs, prompts, and outputs, and developers can then add them to other applications, UIs, or services. Here's an overview of how to make requests to the endpoint and handle the responses. Your API key can be generated using these [steps](quickstart#generate-a-new-api-key). ## Endpoint overview The `/applications` endpoint is designed to generate content based on the inputs provided to a specific no-code application created in AI Studio. You can specify various input parameters as defined in your application. Note: Using the `/applications` endpoint will result in charges for **model usage**. See the [pricing page](/home/pricing) for more information. When calling the `/applications` endpoint, use the `application_id` as the path parameter, which is the unique identifier of a no-code application in AI Studio. The request body should be in JSON format with the following structure: ```json { "inputs": [ { "id": "string", "value": ["string"] } ] } ``` Here's what the request will look like in cURL and the Writer Python and Node SDKs: ```bash cURL curl 'https://api.writer.com/v1/applications/your-application-id' \ -X POST \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer {API_KEY}' \ --data-raw '{ "inputs": [ { "id": "Input name", "value": [ "Input value" ] } ] }' ``` ```python Python application_response = writer.applications.generate_content( application_id="your-application-id", inputs=[ { "id": "Input name", "value": [ "Input value" ] } ], ) ``` ```javascript JavaScript const response = await client.applications.generateContent( "your-application-id", { "inputs": [ { "id": "Input name", "value": [ "Input value" ] } ] }) ``` For each item in the input array, the `id` will be replaced with the unique identifier for the input field from the application. This should be the name of the input type. The `value` property will be the value(s) for the input field. For file inputs, use a `file_id` obtained from the [Files API](../api-guides/api-reference/file-api/upload-files). For convenience, you can simply copy a code snippet from the No-code builder, as shown in the usage example below. A successful request will return a JSON object with the following structure: ```json { "title": "string", "suggestion": "string" } ``` The content of `title` will be the name of the output field. The suggestion will be the response from the model specified in the application. ## Usage example Here's how to use the endpoint in a practical scenario: Use AI Studio to build a no-code text generation application by defining your inputs, prompts, and output formatting. You can follow [this guide](/no-code/building-a-text-generation-app) to get started. ![Text generation application](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/text-generation-app.png) When your application is ready, [follow this guide](/no-code/deploying-an-app) to deploy your application as an embedded application or to Writer. ![Embedding an application](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/embed-app.png) Back in the Build tab, select the "View code" button to display a code snippet. ![View code button in text generation app](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/api/view-code-button.png) Copy the displayed code snippet. You can choose between cURL, Python, or JavaScript. ![Code snippet for text generation app](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/api/applications-snippet.png) Use your preferred HTTP client or one of the SDKs to send a POST request to the endpoint using the code snippet as a starting point. If using cURL, replace `{API_KEY}` with your API key. If using the SDKs, ensure you have set up your `WRITER_API_KEY` in your environment. Add any required input values. ```bash cURL curl 'https://api.writer.com/v1/applications/1234-45090-534590' \ -X POST \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer {API_KEY}' \ --data-raw '{ "inputs": [ { "id": "Product description", "value": [ "Terra running shoe" ] } ] }' ``` ```python Python application_generate_content_response = client.applications.generate_content( application_id="1234-45090-534590", inputs=[ { "id": "Product description", "value": [ "Terra running shoe" ] } ], ) ``` ```javascript JavaScript const response = await client.applications.generateContent( "1234-45090-534590", { "inputs": [ { "id": "Product description", "value": [ "Terra running shoe" ] } ] }, ); ``` Parse the JSON response to access the generated content. Note that outputs without titles will return a value of `null`. Ensure that potential [errors](/api-guides/error-handling), such as timeouts or model errors, are handled gracefully. Be aware of any [rate limits](/api-guides/rate-limits) that may apply to avoid service interruptions. By following this guide, you can integrate the Applications endpoint into your projects to make the most of your no-code applications created in AI Studio. For more info on building no-code applications, check out the [AI Studio No-code documentation](/no-code/introduction). # Chat completion This guide explains the [Chat completion endpoint](./api-reference/completion-api/chat-completion) which can be thought of as having a conversation with our LLM. This endpoint facilitates engaging and dynamic conversations between a user and an AI-assisted chat model. It's designed to handle both multi-turn and single-turn interactions effectively. This guide will walk you through making requests to the endpoint and understanding its responses. Your API key can be generated using these [steps](quickstart#generate-a-new-api-key). ## Endpoint overview This endpoint is designed for generating chat responses based on a conversation history provided in the request. It supports complex, contextual interactions that can simulate a conversational partner. The responses will contain the following variables: * **id**: A unique identifier for the conversation. * **choices**: Objects describing the completion, including the role (`user` or `assistant`) and content. * **index**: The index of the choice. * **message** when `stream` is `false` or **delta** when `stream` is `true`: * **content**: The text generated by the model. Note that the `content` of the first chunk in a streaming response will be `null`. * **role**: The role of the message (e.g. `user` or `assistant`). * **tool\_calls**: The tool calls made during the completion. * **finish\_reason**: Indicates why the response was concluded (`stop`, `length`, `tool_calls`, `content_filter`, `null`). * **created**: The timestamp of the completion. * **model**: The model used for the completion. ## Usage example Here's how you can use the endpoint in a practical scenario: Use your preferred HTTP client or one of our SDKs to send a POST request to `api.writer.com/v1/chat` with the JSON payload. ```bash cURL curl --location 'https://api.writer.com/v1/chat' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' \ --data '{ "model": "palmyra-x-004", "temperature": 0.7, "messages": [ { "role": "user", "content": "You are an expert at writing concise product descriptions for an E-Commerce Retailer" }, { "role": "assistant", "content": "Okay, great I can help write these descriptions. Do you have a specific product in mind?" }, { "role": "user", "content": "Please write a one sentence product description for a cozy, stylish sweater suitable for both casual and formal occasions" } ], "stream": true // or false }' ``` ```python Python chat_response = client.chat.chat( messages=[ { "role": "user", "content": "You are an expert at writing concise product descriptions for an E-Commerce Retailer" }, { "role": "assistant", "content": "Okay, great I can help write these descriptions. Do you have a specific product in mind?" }, { "role": "user", "content": "Please write a one sentence product description for a cozy, stylish sweater suitable for both casual and formal occasions" } ], model="palmyra-x-004", stream=True # or False ) ``` ```javascript JavaScript const chatResponse = await client.chat.chat({ messages: [ { role: "user", content: "You are an expert at writing concise product descriptions for an E-Commerce Retailer" }, { role: "assistant", content: "Okay, great I can help write these descriptions. Do you have a specific product in mind?" }, { role: "user", content: "Please write a one sentence product description for a cozy, stylish sweater suitable for both casual and formal occasions" } ], model: 'palmyra-x-004', stream: true // or false }); ``` For the response parse the JSON to access the generated text. When `stream` is set to `true`, the response is delivered in chunks. The structure remains the same except that you will use `delta` instead of `message`. Note that the `content` of the first chunk in a streaming response will be `null`. ```json stream: true { "id": "1d199764-4a26-4318-8daa-3fafebca83ce", "choices": [ { "index": 0, "finish_reason": "stop", "delta": { "content": "Here's a product description for a cozy, stylish sweater: \"Stay effortlessly stylish with this versatile sweater, perfect for both casual days and formal events, featuring a luxurious feel and elegant design that will keep you cozy and looking your best.\"", "role": "assistant", "tool_calls": null } } ], "created": 1715606709, "model": "palmyra-x-004" } ``` ```json stream: false { "id": "1d199764-4a26-4318-8daa-3fafebca83ce", "choices": [ { "index": 0, "finish_reason": "stop", "message": { "content": "Here's a product description for a cozy, stylish sweater: \"Stay effortlessly stylish with this versatile sweater, perfect for both casual days and formal events, featuring a luxurious feel and elegant design that will keep you cozy and looking your best.\"", "role": "assistant", "tool_calls": null } } ], "created": 1715606709, "model": "palmyra-x-004" } ``` Here are examples of how to handle the response in the SDKs: ```python Python stream: true output_text = "" for chunk in chat_response: if chunk.choices[0].delta.content: output_text += chunk.choices[0].delta.content: else: continue ``` ```python Python stream: false print(chat_response.choices[0].message.content) ``` ```javascript JavaScript stream: true let outputText = ""; for (const chunk of chatResponse) { if (chunk.choices[0]?.delta?.content) { outputText += chunk.choices[0].delta.content; } else { continue; } } ``` ```javascript JavaScript stream: false console.log(chatResponse.choices[0].message.content); ``` Ensure to handle potential [errors](/api-guides/error-handling) such as timeouts or model errors gracefully. Be mindful of any [rate limits](/api-guides/rate-limits) that might apply to avoid service disruptions. ## Best practices Including a system message can guide the behavior of the assistant, setting expectations for its tone and responsiveness. Ensure that all relevant parts of the conversation are included in the messages array to maintain context, as the model doesn't retain memory of past interactions. Implement error handling for various HTTP status codes and API-specific errors (e.g., rate limits or malformed requests). Regularly review the conversation's context and adjust it to keep interactions relevant and concise, especially under the model's token limit. This guide should equip you with the knowledge to integrate and utilize the Chat completion endpoint effectively, enhancing your applications with robust, conversational AI capabilities. # Error handling This table outlines the HTTP status codes you may encounter when using the API, along with descriptions to help you understand and troubleshoot issues. | Error code | Description | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **400** | **Bad request error:** Indicates an issue with the format or content of your request. This error may also encompass other 4XX status errors not specifically listed here. | | **401** | **Authentication error:** There's an issue with your API key, or it's missing from your request headers resulting in unauthorization. | | **403** | **Permission error:** Your API key doesn't have permission to access the requested resource. | | **404** | **Model not found error:** The specified model couldn't be found. This may be due to an incorrect URL or model ID. | | **429** | **Rate limit exceeded:** You have exceeded the allotted number of requests per time period for your account. Please wait and try your request again later. | | **500** | **API error:** An unexpected error occurred within our systems. If this persists, contact support for further assistance. | | **503** | **Service unavailable:** The server is currently unable to handle the request due to a temporary overload or maintenance. Please retry your request after some time. | | **504** | **Timeout error:** The server didn't receive a timely response from an upstream server. Please retry your request after some time. | **Best practices** Ensure that all requests are correctly formatted and include all necessary parameters and authentication information before sending them to the server. Continuously monitor and log errors on your side to identify patterns or recurring issues that may require changes to your application or further investigation. # Introduction The Writer API provides endpoints for integrating Writer generative AI technology into apps or services within your own stack. Use the Writer API if: 1. You need to integrate AI capabilities into a legacy system 2. You want to build a new AI app in the framework or language of your choice 3. You need more flexibility or control over your front-end or back-end than our no-code tools and Writer framework can provide You can access the API endpoints directly or use our SDKs: The Writer Python library provides convenient access to the Writer REST API from any `Python 3.7+` application. The library includes type definitions for all request params and response fields, and offers both synchronous and asynchronous clients powered by `httpx`. This library provides convenient access to the Writer REST API from server-side TypeScript or JavaScript. To get started, head to the [Quickstart](/api-guides/quickstart.mdx). # Knowledge Graph chat support This guide explains how to reference a Knowledge Graph in chat sessions using the tool calling capability of the [Chat completion endpoint](../api-reference/completion-api/chat-completion). This guide will help you understand how to send questions to a Knowledge Graph using [tool calling](../api-guides/tool-calling). Knowledge Graph chat is a predefined tool you can quickly and easily use to reference a Knowledge Graph when users chat with a Palmyra LLM. Your API key can be generated using these [steps](quickstart#generate-a-new-api-key). ## Overview Knowledge Graph chat is a predefined tool supported by Palmyra-X-004 and later to be used with tool calling in the chat endpoint. You make the request as an object inside of the `tools` array with the following parameters: * `type`: The type of tool, which is `graph` for Knowledge Graph chat * `function`: An object containing the following parameters: * `graph_ids`: An array of graph IDs you wish to reference * `description`: A description of the graphs you are referencing * `subqueries`: A boolean indicating whether include the subqueries used by Palmyra in the response To learn how to create a Knowledge Graph and upload files to it, [follow this guide](/api-guides/knowledge-graph). Here are examples of how a Knowledge Graph chat object looks in different programming languages: ```bash cURL "tools": [ { "type": "graph", "function": { "description": "Description of the graph(s)", "graph_ids": [ "your-graph-id" ], "subqueries": true } } ] ``` ```python Python tools = [{ "type": "graph", "function": { "description": "Description of the graph(s)", "graph_ids": [ "your-graph-id" ], "subqueries": True # or False } }] ``` ```js JavaScript const tools = [{ type: "graph", function: { description: "Description of the graph(s)", graph_ids: [ "your-graph-id" ], subqueries: true // or false } }] ``` Knowledge Graph chat is supported with both streaming and non-streaming responses. ## Usage example Here's a practical example of how to use the Knowledge Graph chat tool in your application. To use Knowledge Graph chat, reference it in your tools array: ```bash cURL "tools": [ { "type": "graph", "function": { "description": "Product information graphs", "graph_ids": [ "6029b226-1ee0-4239-a1b0-cdeebfa3ad5a" ], "subqueries": true } } ] ``` ```python Python tools = [{ "type": "graph", "function": { "description": "Product information graphs", "graph_ids": [ "6029b226-1ee0-4239-a1b0-cdeebfa3ad5a" ], "subqueries": True } }] ``` ```js JavaScript const tools = [{ type: "graph", function: { description: "Product information graphs", graph_ids: [ "6029b226-1ee0-4239-a1b0-cdeebfa3ad5a" ], subqueries: true } }] ``` Then, add the tools array to the chat method or endpoint call along with your array of messages. ```bash cURL curl --location 'https://api.writer.com/v1/chat' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' \ --data '{ "model": "palmyra-x-004", "temperature": 0.7, "messages": [ { "role": "user", "content": "Which of our products contain both food coloring and chocolate?" } ], "tool_choice": "auto", "tools": [ { "type": "graph", "function": { "description": "Product information graphs", "graph_ids": [ "6029b226-1ee0-4239-a1b0-cdeebfa3ad5a" ], "subqueries": true } ], "stream": true }' ``` ```python Python messages = [{"role": "user", "content": "Which of our products contain both food coloring and chocolate?"}] response = client.chat.chat( model="palmyra-x-004", messages=messages, tools=tools, tool_choice="auto", stream=True # or False ) ``` ```js JavaScript let messages = [{role: "user", content: "Which of our products contain both food coloring and chocolate?"}] const response = await client.chat.chat( model: "palmyra-x-004", messages: messages, tools: tools, tool_choice: "auto", stream: true // or false ); ``` Finally, process the response from the chat endpoint using the first response in the choices array: ```python Python stream: true response_text="" for chunk in response: if chunk.choices[0].delta.content is not None: response_text += chunk.choices[0].delta.content print(response_text) ``` ```python Python stream: false print(response.choices[0].message.content) ``` ```javascript JavaScript stream: true let content = ''; for await (const chunk of response) { if (chunk.choices[0].delta.content) { content += chunk.choices[0].delta.content; } } console.log(content); ``` ```javascript JavaScript stream: false console.log(response.choices[0].message.content); ``` You may want to display the `sources` or `subqueries` in your UI to assist your user in understanding how the model derived the answer to the question. The full response will have the following structure: ```json stream: true { "id": "1234", "object": "chat.completion.chunk", "choices": [ { "index": 0, "finish_reason": "stop", "delta": { "content": "None of our products contain both chocolate and food coloring. The products containing chocolate are different from those containing food coloring.", "role": "assistant", "tool_calls": null, "graph_data": { "sources": [ { "file_id": "1234", "snippet": "with cocoa for an extra touch of chocolate…" }, { "file_id": "5678", "snippet": "Sugar, corn syrup, artificial flavors, food coloring…" } ], "status": "finished", "subqueries": [ { "query": "Which of our products contain food coloring?", "answer": "The products that contain food coloring are...", "sources": [ { "file_id": "1234", "snippet": "Sugar, citric acid, artificial flavors…" }, { "file_id": "5678", "snippet": "Coffee, coconut milk, ice" } ] }, { "query": "Which of our products contain chocolate?", "answer": "Several products contain chocolate. These include…", "sources": [ { "file_id": "1234", "snippet": "with cocoa for an extra touch of chocolate…" } ] } ] } }, } ] // Other fields omitted for brevity } ``` ```json stream: false { "id": "1234", "object": "chat.completion", "choices": [ { "index": 0, "finish_reason": "stop", "message": { "content": "None of our products contain both chocolate and food coloring. The products containing chocolate are different from those containing food coloring.", "role": "assistant", "tool_calls": null, "graph_data": { "sources": [ { "file_id": "1234", "snippet": "with cocoa for an extra touch of chocolate…" }, { "file_id": "5678", "snippet": "Sugar, corn syrup, artificial flavors, food coloring…" } ], "status": "finished", "subqueries": [ { "query": "Which of our products contain food coloring?", "answer": "The products that contain food coloring are...", "sources": [ { "file_id": "1234", "snippet": "Sugar, citric acid, artificial flavors…" }, { "file_id": "5678", "snippet": "Coffee, coconut milk, ice" } ] }, { "query": "Which of our products contain chocolate?", "answer": "Several products contain chocolate. These include…", "sources": [ { "file_id": "1234", "snippet": "with cocoa for an extra touch of chocolate…" } ] } ] } }, } ] // Other fields omitted for brevity } ``` Note that the subqueries and sources examples shown here have been abbreviated for readability. If the `subqueries` parameter is set to `false`, this array will be empty. By following this guide, you can reference Knowledge Graphs in your users' chats in your application. To learn more about working with files and Knowledge Graphs, check out the [Knowledge Graph guide](../api-guides/knowledge-graph). # Knowledge graph This guide explains the [Knowledge Graph API](./api-reference/kg-api/) and the [File API](./api-reference/file-api/). Knowledge Graph, our graph-based retrieval-augmented generation (RAG), achieves [higher accuracy](https://arxiv.org/abs/2405.02048) than traditional RAG approaches that use vector retrieval. This guide will help you understand and use the Knowledge Graph (KG) API to integrate powerful Retrieval-Augmented Generation (RAG) capabilities into your no-code applications. Our API allows you to seamlessly manage knowledge graphs and their associated files. Whether you’re building chat applications, content recommendation systems, or any other AI-powered tools, the KG API is designed to make your development process efficient and scalable. Your API key can be generated using [these steps](quickstart#generate-a-new-api-key). ## Overview The Knowledge Graph API is a critical part of the Writer suite of tools designed for advanced AI applications. By leveraging knowledge graphs, this API enables developers to create, manage, and use structured data to enhance AI capabilities, particularly in RAG scenarios. ## Usage example Here's how you can use the KG API and File API in a practical scenario: Creating a Knowledge Graph is the first step in organizing your data. You can either make a `POST` call to the `/v1/graphs` endpoint directly or use one of our SDKs. ```bash cURL curl --location --request POST https://api.writer.com/v1/graphs \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --data-raw '{ "name": "financial_reports", "description": "Graph for financial reports" }' ``` ```python Python graph_create_response = client.graphs.create() ``` ```javascript JavaScript const graphCreateResponse = await client.graphs.create(); ``` The response will have this structure: ```json { "id": "6029b226-1ee0-4239-a1b0-cdeebfa3ad5a", "created_at": "2024-06-24T12:34:56Z", "name": "financial_reports", "description": "Graph for financial reports" } ``` You'll need the Knowledge Graph ID i.e. `6029b226-1ee0-4239-a1b0-cdeebfa3ad5a` to associate files to the Graph. You can also [update the name or description of a Knowledge Graph](./api-reference/kg-api/update-graph) with a `PUT` request or [delete the Knowledge Graph](./api-reference/kg-api/delete-graph) with a `DELETE` request. Once you have a Graph, you can start uploading files to Writer. This data is used for retrieval operations in RAG scenarios. See the [pricing](/home/pricing) page for current pricing for file hosting, extraction, and parsing. To use this endpoint, you'll need to add `Content-Type` and `Content-Disposition` headers with the relevant media type and file name. Knowledge Graphs currently support txt, doc, docx, ppt, pptx, jpg, png, eml, html, pdf, srt, csv, xls, and xlsx files. **Endpoint**: `POST /v1/files` ```bash cURL curl --location --request POST 'https://api.writer.com/v1/files' \ --header 'Content-Type: text/plain' \ --header 'Content-Disposition: attachment; filename*=utf-8''test.txt' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data-binary '@/path/to/test.txt' ``` ```python Python file = client.files.upload( content=b'raw file contents', content_disposition='attachment; filename*=utf-8''test.txt', ) ``` ```javascript JavaScript const file = await client.files.upload({ content: fs.createReadStream('path/to/file'), 'Content-Disposition': 'attachment; filename*=UTF-8\'\'test.txt', }); ``` The response will have this structure: ```json { "id": "1862f090-a281-48f3-8838-26c1e78b605e", "created_at": "2024-06-24T12:34:56Z", "name": "test.txt" } ``` You'll need the `file id` i.e. `1862f090-a281-48f3-8838-26c1e78b605e` to associate the files with a Graph. Once a file is uploaded, you can associate it with a Knowledge Graph using the file ID. **Endpoint**: `POST /v1/graphs/{graph_id}/file` ```bash cURL curl --location --request POST https://api.writer.com/v1/graphs/{graph_id}/file \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --data-raw '{"file_id":"1862f090-a281-48f3-8838-26c1e78b605e"}' ``` ```python Python graph_file = client.graphs.add_file_to_graph( graph_id="{graph_id}", file_id="1862f090-a281-48f3-8838-26c1e78b605e", ) ``` ```javascript JavaScript const file = await client.graphs.addFileToGraph('{graph_id}', { file_id: '1862f090-a281-48f3-8838-26c1e78b605e', }); ``` The response will have this structure: ```json { "id": "1862f090-a281-48f3-8838-26c1e78b605e", "created_at": "2024-07-01T20:41:44.159505Z", "name": "test.txt", "graph_ids": [ "6029b226-1ee0-4239-a1b0-cdeebfa3ad5a" ] } ``` To [disassociate a file from a Knowledge Graph](./api-reference/kg-api/remove-file-from-graph.mdx), send a `DELETE` request. The `graph_id` from the response will be needed at later steps, e.g. `6029b226-1ee0-4239-a1b0-cdeebfa3ad5a`. You can retrieve and manage files within your Graphs to ensure that your data is always up-to-date and relevant. **Get all files in all Graphs** **Endpoint**: `GET /v1/files` See the [API reference](./api-reference/file-api/get-all-files) for this endpoint to see all available query parameters. ```bash cURL curl --location --request GET https://api.writer.com/v1/files \ --header "Authorization: Bearer " ``` ```python Python page = client.files.list() page = page.data[0] print(page.id) ``` ```javascript JavaScript for await (const file of client.files.list()) { console.log(file.id); } ``` The response will have the following structure: ```json { "data": [ { "id": "1862f090-a281-48f3-8838-26c1e78b605e", "created_at": "2024-06-24T12:34:56Z", "name": "Q1_report.pdf", "graph_ids": ["6029b226-1ee0-4239-a1b0-cdeebfa3ad5a"] } ], "has_more": false, "first_id": "8ac0a551-6f4b-4779-a0d2-a4464c68ceb4", "last_id": "2449ff3d-0cd6-4188-9e29-f8c7e02c08e4" } ``` **Use query parameters to get files from a specific graph** **Endpoint**: `GET /v1/files?graph_id={graph_id}` ```bash cURL curl --location --request GET 'https://api.writer.com/v1/files?graph_id=6029b226-1ee0-4239-a1b0-cdeebfa3ad5a' \ --header "Authorization: Bearer " ``` ```python Python page = client.files.list(graph_id="6029b226-1ee0-4239-a1b0-cdeebfa3ad5a") page = page.data[0] print(page.id) ``` ```javascript JavaScript for await (const file of client.files.list({ graph_id: '6029b226-1ee0-4239-a1b0-cdeebfa3ad5a' })) { console.log(file.id); } ``` The response will have the following structure: ```json { "data": [ { "id": "5e7d9fa0-d582-40af-8c56-ec9634583eb6", "created_at": "2024-07-15T20:05:52.684560Z", "name": "example.pdf", "graph_ids": [ "6029b226-1ee0-4239-a1b0-cdeebfa3ad5a" ] } ], "has_more": false, "first_id": "5e7d9fa0-d582-40af-8c56-ec9634583eb6", "last_id": "5e7d9fa0-d582-40af-8c56-ec9634583eb6" } ``` **Delete a file** **Endpoint**: `DELETE /v1/files/{fileId}` ```bash cURL curl --location --request DELETE https://api.writer.com/v1/files/5e7d9fa0-d582-40af-8c56-ec9634583eb6 \ --header "Authorization: Bearer " ``` ```python Python file = client.files.delete("5e7d9fa0-d582-40af-8c56-ec9634583eb6") ``` ```javascript JavaScript const fileDeleteResponse = await client.files.delete('5e7d9fa0-d582-40af-8c56-ec9634583eb6'); ``` The response will have the following structure: ```json { "id": "1862f090-a281-48f3-8838-26c1e78b605e", "deleted": true } ``` Ensure to handle potential [errors](/api-guides/error-handling) such as timeouts or model errors gracefully. Be mindful of any [rate limits](/api-guides/rate-limits) that might apply to avoid service disruptions. ## Integrate with AI applications The most powerful aspect of the Knowledge Graph API is its integration with AI applications, enabling advanced RAG capabilities. ### Chat completion with Knowledge Graph Knowledge Graph chat is available as a pre-built tool for use with [tool calling](/api-guides/tool-calling). Check out the [Knowledge Graph chat support guide](/api-guides/kg-chat) to learn how to implement this. ### No-code chat application with Knowledge Graph 1. Create a no-code chat application: Start by building a chat application and configuring the app. Read how to build it [here](/no-code/building-a-chat-app). 2. Enable Knowledge Graph in the application: Make sure you’ve enabled the Knowledge Graph mode and selected the right Graph — the one you sent via the Writer API. Read how to do this [here](/no-code/building-a-chat-app#knowledge-graph-mode). 3. Use Knowledge Graph from the API in a no-code app: Deploy your app to Writer Workspaces or use our embedded application feature to easily embed chat apps with Knowledge Graph in any third-party software. Check out more details [here](/no-code/building-a-chat-app#knowledge-graph-mode). # Legacy API reference # Quickstart Follow these steps to create your API app and generate a Writer API key, allowing you to authenticate requests to your app's API. ## Start building your app From the Home screen, click on **Build an app**. ![Build an app](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/api/home.png) Select **API** as the app type you'd like to create, enabling you to generate keys and build your app with API calls. ![API app](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/api/step1.png) Provide a **short description** of your app in the specified field to give an overview of its purpose or functionality. ![API app description](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/api/step2.png) An app ID (e.g., `ek_test_TYooMQauvdEDq54NiTphI7jx`) is automatically assigned to your app. This unique ID identifies your app within the system. ## Generate a new API key Within the API Key section, click the **Generate a new key** option. You'll need to name your API key before you can generate it. Fill in the key name field with a descriptive name for your key. ![Generate key](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/api/step3.jpg) Click **Generate** after naming your key to create it. ![Generate Click](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/api/step4.png) Upon generating the key, a credential (API key) will be displayed. ![Key Creation](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/api/step5.png) With your API key generated and your app’s capabilities configured, your app is ready for use. The API key enables authorization to the Writer API when used as a `Bearer` token. ## Configuring capabilities Your API app includes various capabilities like list models, text generation, chat completion, etc. Explore each capability and configure them for your app as needed, using the provided documentation links for guidance. ![Key on Home](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/api/step6.png) ## Managing API keys In the API keys section, all your generated keys are listed along with their details such as key name, creation date, and last usage date. Actions such as renaming or revoking keys can be performed here. Remember, revoking a key is irreversible. ![Revoke Key](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/api/step7.jpg) ## List models To see all available models, use the following request: ```bash cURL curl --location 'https://api.writer.com/v1/models' \ --header 'Authorization: Bearer {apiKey}' ``` ```python Python models = client.models.list() ``` ```javascript JavaScript const models = await client.models.list(); ``` # Rate limits For custom rate limits please **[contact our sales team](https://go.writer.com/contact-sales)**. To ensure optimal service performance and fairness in resource allocation, our endpoints enforce the following rate limits. 1. **RPM (requests per minute)**: 400 2. **TPM (token per min)**: 25,000 ## Best practices Implement mechanisms in your applications to track and regulate the frequency of your requests to stay within the prescribed limits. In cases where you exceed these limits, employ adaptive retry strategies with exponential backoff to handle retries efficiently and reduce the likelihood of consecutive limit breaches. Prepare to handle HTTP 429 (too many requests) responses by pausing or slowing down request rates. # SDKs This guide will help you get started with the Writer SDKs. Follow these steps to install the SDKs and perform basic operations. Your API key can be generated using these [steps](quickstart#generate-a-new-api-key). ## Prerequisites * Python 3.7 or higher and pip installed on your system * A Writer API key ## Installation Open your terminal or command prompt and install the Writer SDK using npm: ```sh pip install writer-sdk ``` ## Setting up the Writer client Create a new Python file (e.g., `writer-test.py`). Import the Writer SDK and create a client instance, replacing `your-api-key` with your actual API key: ```python from writerai import Writer client = Writer( api_key="your-api-key" # Replace with your API key ) ``` For production usage, we recommend using a library like [python-dotenv](https://pypi.org/project/python-dotenv/) to manage your API key in a `.env` file as `WRITER_API_KEY`. ## Basic usage: Chat completion To get started using Writer LLMs and the API, perform a basic chat completion task. Add the following code to your file: ```python def main(): try: response = client.chat.chat( messages=[{ "content": "Write a poem about Python", "role": 'user' }], model="palmyra-x-004", stream=True, ) output_text = "" for chunk in response: if chunk.choices[0].delta.content: output_text += chunk.choices[0].delta.content: else: continue print(output_text) except Exception as error: print("Error:", error) if __name__ == "__main__": main() ``` Save the file and run it using Python: ```sh python writer-test.py ``` You should see the chat response streaming to the console. For advanced SDK usage, such as custom requests or configuring retries and timeouts, refer to the README file in the [GitHub repository](https://github.com/writer/writer-python). ## Prerequisites * Node.js and npm installed on your system * A Writer API key ## Installation Open your terminal or command prompt and install the Writer SDK using npm: ```sh npm install writer-sdk ``` ## Setting up the Writer client Create a new JavaScript file (e.g., `writer-test.js`). Import the Writer SDK and create a client instance, replacing `your-api-key` with your actual API key: ```javascript const Writer = require('writer-sdk'); const client = new Writer({ apiKey: 'your-api-key', // Replace with your actual API key }); ``` For production usage, store your API key in an environment variable called `WRITER_API_KEY` for security. ## Basic usage: Chat completion To get started using Writer LLMs and the API, perform a basic chat completion task. Add the following code to your file: ```javascript async function main() { try { const response = await client.chat.chat({ messages: [{ content: 'Write a poem about Node', role: 'user' }], model: 'palmyra-x-004', stream: true }); let outputText = ""; for (const chunk of response) { if (chunk.choices[0]?.delta?.content) { outputText += chunk.choices[0].delta.content; } else { continue; } } console.log(outputText); } catch (error) { console.error('Error:', error); } } main(); ``` Save the file and run it using Node.js: ```sh node writer-test.js ``` You should see the chat response streaming to the console. For advanced SDK usage, such as custom requests or configuring retries and timeouts, refer to the README file in the [GitHub repository](https://github.com/writer/writer-node). ## Next steps Now that you're set up with the SDKs, check out the guides on [text generation](./text-generation), [applications](./applications), and [Knowledge Graph](./knowledge-graph) to explore more advanced features of the Writer platform. You can also use the [API reference](./api-reference/) to learn more detailed information about available endpoints. # Text generation This guide explains the [Text generation endpoint](../api-reference/completion-api/text-generation) which can be thought of as asking the Palmyra LLM a single question. This Text Generation guide is intended to help you understand how to interact with the Text Generation API effectively. Below, you'll find an overview of how to make requests to the endpoint, and handle the responses. Your API key can be generated using these [steps](quickstart#generate-a-new-api-key). ## Endpoint overview This endpoint is designed to generate text based on the input parameters provided. You can specify various aspects of the completion request, such as the model, prompt, maximum number of tokens, temperature, and streaming behavior. The response will include the generated text along with the model used. The structure differs based on the value of the stream parameter. If stream is set to `true`, the response is delivered in chunks, each encapsulated as a separate JSON object. Multiple such chunks will be sent until the entire completion is transmitted. If stream is set to `false`, the response is a JSON object containing the generated text and the model used: ## Usage example Here's how you can use the endpoint in a practical scenario: Use your preferred HTTP client or one of our SDKs to send a POST request to `api.writer.com/v1/completions` with the JSON payload. ```bash cURL curl --location 'https://api.writer.com/v1/completions' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' \ --data '{ "model": "palmyra-x-003-instruct", "prompt": "Tell me a story", "max_tokens": 1000, "temperature": 0.7, "stream": true // or false }' ``` ```python Python completion = client.completion.create( model="palmyra-x-003-instruct", prompt="Tell me a story", max_tokens=1000, temperature=0.7, stream=True # or False ) ``` ```javascript JavaScript const completion = await client.completions.create({ model: 'palmyra-x-003-instruct', prompt: 'Tell me a story', max_tokens: 1000, temperature: 0.7, stream: true // or false }); ``` For non-streaming responses, parse the JSON to access the generated text. For streaming responses, handle each chunk as it arrives and concatenate or process it as needed. ```json Response stream: true // Stream response is delivered in chunks data: { "value": "Chunk 1 of generated story here..." } data: { "value": "Chunk 2 of generated story here..." } ``` ```json Response stream: false { "choices": [ { "text": "Generated story here..." } ], "model": "palmyra-x-003-instruct" } ``` Here are examples of how to handle the response in the SDKs: ```python Python stream: true for chunk in completion: print(chunk.value) ``` ```python Python stream: false print(completion.choices[0].text) ``` ```javascript JavaScript stream: true for await (const chunk of completion) { console.log(chunk.value); } ``` ```javascript JavaScript stream: false console.log(completion.choices[0].text); ``` Ensure to handle potential [errors](/api-guides/error-handling) such as timeouts or model errors gracefully. Be mindful of any [rate limits](/api-guides/rate-limits) that might apply to avoid service disruptions. By following this guide, you can integrate our Text Generation endpoint into your applications to leverage powerful text generation capabilities for various purposes, enhancing user interaction and content creation. # Tool calling This guide explains how tool calling works with the [Chat completion endpoint](../api-reference/completion-api/chat-completion). This guide will help you understand how to effectively use tool calling, sometimes known as function calling. Tool calling allows you to extend the capabilities of AI applications by enabling direct interaction between models and predefined functions. These functions can perform a wide range of tasks, such as querying databases, fetching real-time data from APIs, processing and manipulating data, or executing business logic. The result of these tool calls can then be integrated back into the model's output. Your API key can be generated using these [steps](quickstart#generate-a-new-api-key). ## Overview Tool calling follows these steps: * Defining your functions in code * Describing your functions as tools to the model * Checking to see which tools were invoked by the model and running the corresponding functions * Passing the results of the tool call back to the model Let's look at each one. ## Defining functions First, define your functions in your code. Typical use cases for tool calling include calling an API (e.g. an external service or database), performing math calculations, or running complex business logic. Here's an example of defining a function to calculate the mean of a list of numbers: ```python Python def calculate_mean(numbers: list) -> float: return sum(numbers) / len(numbers) ``` ```js JavaScript function calculateMean(numbers) { if (numbers.length === 0) { throw new Error("Cannot calculate mean of an empty array"); } return numbers.reduce((sum, num) => sum + num, 0) / numbers.length; } ``` ## Describing functions as tools With your functions defined, you'll next need to define a `tools` array that you will pass to the model in order to describe your functions as tools available to the model. You describe tools in the form of a [JSON schema](https://json-schema.org/). Each tool should include a `type` of `function` and a `function` object that includes a `name`, `description`, and a dictionary of `parameters`. ```python Python tools = [ { "type": "function", "function": { "name": "calculate_mean", "description": "Calculate the mean (average) of a list of numbers.", "parameters": { "type": "object", "properties": { "numbers": { "type": "array", "items": {"type": "number"}, "description": "List of numbers" } }, "required": ["numbers"] } } } ] ``` ```js JavaScript const tools = [ { type: "function", function: { name: "calculate_mean", description: "Calculate the mean (average) of a list of numbers.", parameters: { type: "object", properties: { numbers: { type: "array", items: { type: "number" }, description: "List of numbers", }, }, required: ["numbers"], }, }, }, ]; ``` ## Passing tools to the model Once the tools array is complete, you will pass it to the chat completions endpoint or SDK method along with your model and messages. Set `tool_choice` to `auto` to take full advantage of the model's capabilities. You can use tool calling with `stream` set to either `true` or `false`. When `stream` is set to `true`, you'll access the response message using `delta` instead of `message` in the `choices` array. Note: Tool calling is only available in Palmyra-X-004. ```python Python messages = [{"role": "user", "content": "what is the mean of [1,3,5,7,9]?"}] response = client.chat.chat( model="palmyra-x-004", messages=messages, tools=tools, tool_choice="auto", stream=False # Set to True if you want to use streaming ) ``` ```js JavaScript let messages = [{"role": "user", "content": "what is the mean of [1,3,5,7,9]?"}]; const response = await client.chat.chat({ model: "palmyra-x-004", messages: messages, tools: tools, tool_choice: "auto", stream: false // Set to true if you want to use streaming }); ``` ## Processing tool calls When the model identifies a need to call a tool based on the user's input, it invokes it in the response, passing along any necessary parameters. You then execute the tool's function and return the result to the model. The method for checking for tool calls and executing the tool's function differs depending on whether you're using streaming or non-streaming. When using streaming, the tool calls will come back in chunks inside of the `delta` object of the `choices` array. You'll iterate through the response chunks to check for tool calls, concatenate the streaming tool call content, execute the functions, and append the function call results to the `messages` array. Iterate through the response chunks to check for tool calls, concatenate the streaming tool call content, and handle non-tool-call content (i.e. if the user asks a question not requiring a tool call): ```python Python streaming_content = "" function_calls = [] for chunk in response: choice = chunk.choices[0] if choice.delta: # Check for tool calls if choice.delta.tool_calls: for tool_call in choice.delta.tool_calls: if tool_call.id: # Append an empty dictionary to the function_calls list with the tool call ID function_calls.append( {"name": "", "arguments": "", "call_id": tool_call.id} ) if tool_call.function: # Append function name and arguments to the last dictionary in the function_calls list function_calls[-1]["name"] += ( tool_call.function.name if tool_call.function.name else "" ) function_calls[-1]["arguments"] += ( tool_call.function.arguments if tool_call.function.arguments else "" ) # Handle non-tool-call content elif choice.delta.content: streaming_content += choice.delta.content ``` ```js JavaScript let streamingContent = ""; const functionCalls = []; for await (const chunk of response) { const choice = chunk.choices[0]; if (choice.delta) { if (choice.delta.tool_calls) { for (const toolCall of choice.delta.tool_calls) { if (toolCall.id) { functionCalls.push({ name: "", arguments: "", call_id: toolCall.id }); } if (toolCall.function) { functionCalls[functionCalls.length - 1].name += toolCall.function.name || ""; functionCalls[functionCalls.length - 1].arguments += toolCall.function.arguments || ""; } } } else if (choice.delta.content) { streamingContent += choice.delta.content; } } } ``` While inside of the loop and the if-statement for `choice.delta`, check for the `finish_reason` of the `choice`. If the `finish_reason` is `stop`, this means the model has finished generating the response and no tools have been called. If the `finish_reason` is `tool_calls`, call each function in the `function_calls` list and append the result to the `messages` array. Be sure to convert the function response to a string before appending it to the messages array. ```python Python # Inside of the loop and the if-statement for `choice.delta` # A finish reason of stop means the model has finished generating the response if choice.finish_reason == "stop": messages.append({"role": "assistant", "content": streaming_content}) # A finish reason of tool_calls means the model has finished deciding which tools to call elif choice.finish_reason == "tool_calls": for function_call in function_calls: if function_call["name"] == "calculate_mean": arguments_dict = json.loads(function_call["arguments"]) function_response = calculate_mean(arguments_dict["numbers"]) messages.append( { "role": "tool", "content": str(function_response), "tool_call_id": function_call["call_id"], "name": function_call["name"], } ) ``` ```js JavaScript // Inside of the loop and the if-statement for `choice.delta` // A finish reason of stop means the model has finished generating the response if (choice.finish_reason === "stop") { messages.push({ role: "assistant", content: streamingContent }); } else if (choice.finish_reason === "tool_calls") { // A finish reason of tool_calls means the model has finished deciding which tools to call for (const functionCall of functionCalls) { if (functionCall.name === "calculate_mean") { const argumentsDict = JSON.parse(functionCall.arguments); const functionResponse = await calculateMean(argumentsDict.numbers); messages.push({ role: "tool", content: functionResponse.toString(), tool_call_id: functionCall.call_id, name: functionCall.name, }); } } ``` After you've appended the tool call results to the messages array, you can pass the messages array back to the model to get the final response. Note that this code block should be inside of the check for the `finish_reason` of `tool_calls`, after the loop that iterates through the `function_calls` list: ```python Python # Inside of `elif choice.finish_reason == "tool_calls"` final_response = client.chat.chat( model="palmyra-x-004", messages=messages, stream=True ) final_streaming_content = "" for chunk in final_response: choice = chunk.choices[0] if choice.delta and choice.delta.content: final_streaming_content += choice.delta.content print(final_streaming_content) # The mean is 5 ``` ```js JavaScript // Inside of `else if (choice.finish_reason === "tool_calls")` const finalResponse = await client.chat.chat({ model: "palmyra-x-004", messages: messages, stream: true }); let finalStreamingContent = ""; for await (const chunk of finalResponse) { const choice = chunk.choices[0]; if (choice.delta && choice.delta.content) { finalStreamingContent += choice.delta.content; } } console.log(finalStreamingContent); // The mean is 5 ``` Here is the full code example for streaming tool calling: ```python Python import json import dotenv from writerai import Writer dotenv.load_dotenv() client = Writer() def calculate_mean(numbers: list) -> float: return sum(numbers) / len(numbers) tools = [ { "type": "function", "function": { "name": "calculate_mean", "description": "Calculate the mean (average) of a list of numbers.", "parameters": { "type": "object", "properties": { "numbers": { "type": "array", "items": {"type": "number"}, "description": "List of numbers" } }, "required": ["numbers"] } } } ] messages = [{"role": "user", "content": "what is the mean of [1,3,5,7,9]?"}] response = client.chat.chat( model="palmyra-x-004", messages=messages, tools=tools, tool_choice="auto", stream=True ) streaming_content = "" function_calls = [] for chunk in response: choice = chunk.choices[0] if choice.delta: # Check for tool calls if choice.delta.tool_calls: for tool_call in choice.delta.tool_calls: if tool_call.id: # Append an empty dictionary to the function_calls list with the tool call ID function_calls.append( {"name": "", "arguments": "", "call_id": tool_call.id} ) if tool_call.function: # Append function name and arguments to the last dictionary in the function_calls list function_calls[-1]["name"] += ( tool_call.function.name if tool_call.function.name else "" ) function_calls[-1]["arguments"] += ( tool_call.function.arguments if tool_call.function.arguments else "" ) # Handle non-tool-call content elif choice.delta.content: streaming_content += choice.delta.content # A finish reason of stop means the model has finished generating the response if choice.finish_reason == "stop": messages.append({"role": "assistant", "content": streaming_content}) # A finish reason of tool_calls means the model has finished deciding which tools to call elif choice.finish_reason == "tool_calls": for function_call in function_calls: if function_call["name"] == "calculate_mean": arguments_dict = json.loads(function_call["arguments"]) function_response = calculate_mean(arguments_dict["numbers"]) messages.append( { "role": "tool", "content": str(function_response), "tool_call_id": function_call["call_id"], "name": function_call["name"], } ) final_response = client.chat.chat( model="palmyra-x-004", messages=messages, stream=True ) final_streaming_content = "" for chunk in final_response: choice = chunk.choices[0] if choice.delta and choice.delta.content: final_streaming_content += choice.delta.content print(final_streaming_content) # The mean is 5 ``` ```js JavaScript const dotenv = require("dotenv"); const Writer = require("writer-sdk"); dotenv.config(); const client = new Writer(); function calculateMean(numbers) { if (numbers.length === 0) { throw new Error("Cannot calculate mean of an empty array"); } return numbers.reduce((sum, num) => sum + num, 0) / numbers.length; } const tools = [ { type: "function", function: { name: "calculate_mean", description: "Calculate the mean (average) of a list of numbers.", parameters: { type: "object", properties: { numbers: { type: "array", items: { type: "number" }, description: "List of numbers", }, }, required: ["numbers"], }, }, }, ]; async function main() { let messages = [ { role: "user", content: "what is the mean of [1,3,5,7,9]?" }, ]; const response = await client.chat.chat({ model: "palmyra-x-004", messages: messages, tools: tools, tool_choice: "auto", stream: true, }); let streamingContent = ""; const functionCalls = []; for await (const chunk of response) { const choice = chunk.choices[0]; if (choice.delta) { if (choice.delta.tool_calls) { for (const toolCall of choice.delta.tool_calls) { if (toolCall.id) { functionCalls.push({ name: "", arguments: "", call_id: toolCall.id, }); } if (toolCall.function) { functionCalls[functionCalls.length - 1].name += toolCall.function.name || ""; functionCalls[functionCalls.length - 1].arguments += toolCall.function.arguments || ""; } } } else if (choice.delta.content) { streamingContent += choice.delta.content; } // A finish reason of stop means the model has finished generating the response if (choice.finish_reason === "stop") { messages.push({ role: "assistant", content: streamingContent }); } else if (choice.finish_reason === "tool_calls") { console.log(functionCalls); // A finish reason of tool_calls means the model has finished deciding which tools to call for (const functionCall of functionCalls) { if (functionCall.name === "calculate_mean") { const argumentsDict = JSON.parse( functionCall.arguments ); const functionResponse = calculateMean( argumentsDict.numbers ); messages.push({ role: "tool", content: functionResponse.toString(), tool_call_id: functionCall.call_id, name: functionCall.name, }); } } const finalResponse = await client.chat.chat({ model: "palmyra-x-004", messages: messages, stream: true, }); let finalStreamingContent = ""; for await (const chunk of finalResponse) { const choice = chunk.choices[0]; if (choice.delta && choice.delta.content) { finalStreamingContent += choice.delta.content; } } console.log(finalStreamingContent); // The mean is 5 } } } } main(); ``` When `stream` is set to `false`, the tool calls will come back in one object inside of the `messages` object in the `choices` array. First, check for the invocation of the tool. If the tool is called, run the tool's function with the provided arguments: ```python Python response_message = response.choices[0].message tool_calls = response_message.tool_calls if tool_calls: tool_call = tool_calls[0] tool_call_id = tool_call.id function_name = tool_call.function.name function_args = json.loads(tool_call.function.arguments) if function_name == "calculate_mean": function_response = calculate_mean(function_args["numbers"]) ``` ```js JavaScript const responseMessage = response.choices[0].message; const toolCalls = responseMessage.tool_calls; if (toolCalls && toolCalls.length > 0) { const toolCall = toolCalls[0]; const toolCallId = toolCall.id; const functionName = toolCall.function.name; const functionArgs = JSON.parse(toolCall.function.arguments); if (functionName === "calculate_mean") { const functionResponse = calculateMean(functionArgs.numbers); } } ``` Then, pass the result back to the model by appending it to the messages array. Be sure to convert the function response to a string if necessary before appending it to the messages array. ```python Python # Within the if statement for tool call messages.append({ "role": "tool", "tool_call_id": tool_call_id, "name": function_name, "content": str(function_response), }) ``` ```js JavaScript // Within the if statement for tool call messages.push({ role: "tool", tool_call_id: toolCallId, name: functionName, content: functionResponse.toString(), }); ``` After you've appended the tool call results to the messages array, you can pass the messages array back to the model to get the final response. ```python Python final_response = client.chat.chat( model="palmyra-x-004", messages=messages, stream=False ) print(f"Final response: \n{final_response.choices[0].message.content}\n") # Final response: "The mean is 5" ``` ```js JavaScript const finalResponse = await client.chat.chat({ model: "palmyra-x-004", messages: messages, stream: false }); console.log(`Final response: \n${finalResponse.choices[0].message.content}\n`); // Final response: "The mean is 5" ``` Here is the full code example for non-streaming tool calling: ```python Python import json import dotenv from writerai import Writer dotenv.load_dotenv() client = Writer() def calculate_mean(numbers: list) -> float: return sum(numbers) / len(numbers) tools = [ { "type": "function", "function": { "name": "calculate_mean", "description": "Calculate the mean (average) of a list of numbers.", "parameters": { "type": "object", "properties": { "numbers": { "type": "array", "items": {"type": "number"}, "description": "List of numbers" } }, "required": ["numbers"] } } } ] messages = [{"role": "user", "content": "what is the mean of [1,3,5,7,9]?"}] response = client.chat.chat( model="palmyra-x-004", messages=messages, tools=tools, tool_choice="auto", stream=False ) response_message = response.choices[0].message tool_calls = response_message.tool_calls if tool_calls: tool_call = tool_calls[0] tool_call_id = tool_call.id function_name = tool_call.function.name function_args = json.loads(tool_call.function.arguments) if function_name == "calculate_mean": function_response = calculate_mean(function_args["numbers"]) messages.append({ "role": "tool", "tool_call_id": tool_call_id, "name": function_name, "content": str(function_response), }) final_response = client.chat.chat( model="palmyra-x-004", messages=messages, stream=False ) print(f"Final response: \n{final_response.choices[0].message.content}\n") # Final response: "The mean is 5" ``` ```js JavaScript const dotenv = require("dotenv"); const Writer = require("writer-sdk"); dotenv.config(); const client = new Writer(); function calculateMean(numbers) { if (numbers.length === 0) { throw new Error("Cannot calculate mean of an empty array"); } return numbers.reduce((sum, num) => sum + num, 0) / numbers.length; } const tools = [ { type: "function", function: { name: "calculate_mean", description: "Calculate the mean (average) of a list of numbers.", parameters: { type: "object", properties: { numbers: { type: "array", items: { type: "number" }, description: "List of numbers", }, }, required: ["numbers"], }, }, }, ]; async function main() { let messages = [ { role: "user", content: "what is the mean of [1,3,5,7,9]?" }, ]; const response = await client.chat.chat({ model: "palmyra-x-004", messages: messages, tools: tools, tool_choice: "auto", stream: false, }); const responseMessage = response.choices[0].message; const toolCalls = responseMessage.tool_calls; if (toolCalls && toolCalls.length > 0) { const toolCall = toolCalls[0]; const toolCallId = toolCall.id; const functionName = toolCall.function.name; const functionArgs = JSON.parse(toolCall.function.arguments); if (functionName === "calculate_mean") { const functionResponse = calculateMean(functionArgs.numbers); messages.push({ role: "tool", tool_call_id: toolCallId, name: functionName, content: functionResponse.toString(), }); } } const finalResponse = await client.chat.chat({ model: "palmyra-x-004", messages: messages, stream: false }); console.log(`Final response: \n${finalResponse.choices[0].message.content}\n`); // Final response: "The mean is 5" } main(); ``` ## Usage example Let's look at a common use case for tool calling: calling an external API. This example will use a function to call an API that returns information about a product based on its ID. This example is using non-streaming; for streaming, refer to the example above to adjust the code. First, define the function in your code, replacing the API key and URL with your actual values: ```python Python def get_product_info(product_id): url = f"http://api.company.com/?apikey={api_key}&id={product_id}" response = requests.get(url) if response.status_code == 200: return json.dumps(response.json()) else: return f"Failed to retrieve product info. Status code: {response.status_code}" ``` ```js JavaScript async function getProductInfo(productId) { const apiKey = "your_api_key"; // Replace with your actual API key const url = `http://api.company.com/?apikey=${apiKey}&id=${productId}`; try { const response = await fetch(url); if (response.ok) { const data = await response.json(); return JSON.stringify(data); } else { return `Failed to retrieve product info. Status code: ${response.status}`; } } catch (error) { return `Error fetching product info: ${error.message}`; } } ``` Next, define a tools array that describes the tool with a JSON schema: ```python Python tools = [ { "type": "function", "function": { "name": "get_product_info", "description": "Get information about a product by its id", "parameters": { "type": "object", "properties": { "product_id": { "type": "number", "description": "The unique identifier of the product to retrieve information for", } }, "required": ["product_id"], }, }, } ] ``` ```js JavaScript const tools = [ { type: "function", function: { name: "get_product_info", description: "Get information about a product by its id", parameters: { type: "object", properties: { product_id: { type: "number", description: "The unique identifier of the product to retrieve information for" } }, required: ["product_id"] } } } ]; ``` Pass the tools array to the chat completions endpoint or SDK method along with your model and messages. Set `tool_choice` to `auto`. ```python Python messages = [{"role": "user", "content": "what is the name of the product with id 12345?"}] response = client.chat.chat( model="palmyra-x-004", messages=messages, tools=tools, tool_choice="auto", stream=False ) ``` ```js JavaScript let messages = [{role: "user", content: "what is the name of the product with id 12345?"}]; let response = client.chat.chat( model="palmyra-x-004", messages=messages, tools=tools, tool_choice="auto", stream=false ); ``` Loop through the `tool_calls` array to check for the invocation of the tool. Then, call the tool's function with the arguments. ```python Python response_message = response.choices[0].message messages.append(response_message) tool_calls = response_message.tool_calls if tool_calls: tool_call = tool_calls[0] tool_call_id = tool_call.id function_name = tool_call.function.name function_args = json.loads(tool_call.function.arguments) if function_name == "get_product_info": function_response = get_product_info(function_args["product_id"]) ``` ```js JavaScript const toolCalls = responseMessage.tool_calls; if (toolCalls && toolCalls.length > 0) { const toolCall = toolCalls[0]; const toolCallId = toolCall.id; const functionName = toolCall.function.name; const functionArgs = JSON.parse(toolCall.function.arguments); if (functionName === "get_product_info") { const functionResponse = getProductInfo(functionArgs.product_id); } } ``` Finally, pass the result back to the model by appending it to the messages array and get the final response: ```python Python messages.append({ "role": "tool", "tool_call_id": tool_call_id, "name": function_name, "content": function_response, }) final_response = client.chat.chat( model="palmyra-x-004", messages=messages, stream=False ) print(f"Final response: {final_response.choices[0].message.content}") # Final response:: Product ID 12345 is the Terra Running Shoe. ``` ```js JavaScript messages.append({ role: "tool", tool_call_id: tool_call_id, name: function_name, content: function_response, }); const finalResponse = client.chat.chat( model="palmyra-x-004", messages=messages, stream=false ); console.log(`Final response: ${finalResponse.choices[0].message.content}`) // Final response: Product ID 12345 is the Terra Running Shoe. ``` By following this guide, you can incorporate tool calling into your application and augment the capabilities of a model with real-time data, math operations, business logic, and much more. For more examples, check out the [tool calling cookbooks](https://github.com/writer/cookbooks/tree/main/tool_calling) available on GitHub. To learn how to use tool calling to chat with Knowledge Graph, read [this guide](../api-guides/kg-chat). # Tools API This guide explains the [Tools API](../api-guides/api-reference/tool-api/), which provides various text analysis and processing capabilities. This guide covers the key endpoints available in Writer's Tools API, which provides various text analysis and processing capabilities. Each of these endpoints incurs different usage charges. See the [pricing page](/home/pricing) for more information. Your API key can be generated using these [steps](quickstart#generate-a-new-api-key). ## Context-aware text splitting The [context-aware splitting endpoint](../api-guides/api-reference/tool-api/context-aware-splitting) provides intelligent text splitting capabilities for long documents (up to 4000 words). Unlike simple character-based splitting, it preserves the semantic meaning and context between chunks, making it ideal for processing long-form content while maintaining coherence. ### Use cases * Breaking down long articles or research papers for improved readability * Preparing content for chunked processing in RAG (Retrieval-Augmented Generation) systems * Splitting lengthy legal documents while maintaining context around clauses and references * Creating digestible sections of educational content while preserving logical flow * Processing large documentation files for knowledge base creation ### Request * `text`: The text content to be split (required) * `strategy`: The splitting strategy to use (required). Options include: * `llm_split`: Uses language model for precise semantic splitting * `fast_split`: Uses heuristic-based approach for quick splitting * `hybrid_split`: Combines both approaches ### Response Returns an array of text chunks, with at least one chunk guaranteed. Each chunk maintains semantic coherence while preserving the context of the original text. ### Example code Here's how to use the context-aware text splitting endpoint: ```bash cURL curl --location 'https://api.writer.com/v1/tools/context-aware-splitting' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer WRITER_API_KEY' \ --data '{ "text": "text to split", "strategy": "llm_split" }' ``` ```python Python import os from writerai import Writer client = Writer( # This is the default and can be omitted api_key=os.environ.get("WRITER_API_KEY"), ) response = client.tools.context_aware_splitting( strategy="llm_split", text="text to split", ) print(response.chunks) ``` ```javascript JavaScript import Writer from 'writer-sdk'; const client = new Writer({ apiKey: process.env['WRITER_API_KEY'], // This is the default and can be omitted }); async function main() { const response = await client.tools.contextAwareSplitting({ strategy: 'llm_split', text: 'text to split' }); console.log(response.chunks); } main(); ``` ## Medical comprehend The [medical comprehend endpoint](../api-guides/api-reference/tool-api/comprehend-medical) analyzes unstructured medical text to extract entities and label them with standardized medical codes. Each extracted entity comes with a confidence score, making it useful for processing clinical notes, medical records, and other healthcare-related documents. ### Use cases * Automating medical records processing and classification * Extracting diagnosis codes from clinical notes for billing and insurance purposes * Creating structured datasets from unstructured medical documentation * Identifying and categorizing medications and their attributes in patient records * Standardizing medical terminology across different healthcare systems using SNOMED CT codes ### Request * `content`: The medical text to analyze (required) * `response_type`: The desired response format (required). Options include: * `Entities`: Returns medical entities with their categories. * `RxNorm`: [RxNorm](https://www.nlm.nih.gov/research/umls/rxnorm/overview.html) provides normalized names and unique identifiers for medicines and drugs, allowing computer systems to communicate drug-related information efficiently and unambiguously. * `ICD-10-CM`: [ICD-10-CM](https://www.cdc.gov/nchs/icd/icd-10-cm/index.html) is a standardized system used to code diseases and medical conditions (morbidity) data. * `SNOMED CT`: [SNOMED CT](https://www.snomed.org/what-is-snomed-ct) is a standardized, multilingual vocabulary of clinical terminology that is used by physicians and other healthcare providers for the electronic exchange of health information. ### Response Returns an array of medical entities, where each entity includes: * `category`: The medical category of the entity * `text`: The actual text that was identified * `score`: Confidence score for the entity (0-1) * `traits`: Array of trait objects with names and scores * `concepts`: Array of medical concepts with codes and descriptions * `attributes`: Related attributes with their own scores and relationships * `type`: The entity type * Position information (`begin_offset` and `end_offset`) ### Example code Here's how to analyze medical text using the medical comprehend endpoint: ```bash cURL curl --location 'https://api.writer.com/v1/tools/comprehend/medical' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer WRITER_API_KEY' \ --data '{ "content": "the symptoms are soreness, a temperature and cough", "response_type": "SNOMED CT" }' ``` ```python Python import os from writerai import Writer client = Writer( # This is the default and can be omitted api_key=os.environ.get("WRITER_API_KEY"), ) medical = client.tools.comprehend.medical( content="the symptoms are soreness, a temperature and cough", response_type="Entities", ) print(medical.entities) ``` ```javascript JavaScript import Writer from 'writer-sdk'; const client = new Writer({ apiKey: process.env['WRITER_API_KEY'], // This is the default and can be omitted }); async function main() { const medical = await client.tools.comprehend.medical({ content: 'the symptoms are soreness, a temperature and cough', response_type: 'Entities' }); console.log(medical.entities); } main(); ``` ## PDF parser The [PDF parser endpoint](../api-guides/api-reference/tool-api/pdf-parser) converts PDF documents into other formats. This is particularly useful when you need to extract and process text content from PDF files for further analysis or integration into your workflow. ### Use cases * Converting research papers from PDF to searchable text for analysis * Extracting content from business reports for data processing * Converting PDF documentation into markdown format for web publishing * Making archived PDF documents searchable and analyzable * Automating data extraction from PDF forms and invoices ### File upload Before using the PDF parser, you'll need to upload your PDF file to Writer to obtain a file ID. Use the [files API upload endpoint](../api-guides/api-reference/file-api/upload-files) to upload your document: ```bash cURL curl --location --request POST 'https://api.writer.com/v1/files' \ --header 'Content-Type: application/pdf' \ --header 'Content-Disposition: attachment; filename*=utf-8''document.pdf' \ --header 'Authorization: Bearer WRITER_API_KEY' \ --data-binary '@/path/to/document.pdf' ``` ```python Python file = client.files.upload( content=b'raw file contents', content_disposition='attachment; filename*=utf-8''document.pdf', ) ``` ```javascript JavaScript const file = await client.files.upload({ content: fs.createReadStream('path/to/file'), 'Content-Disposition': 'attachment; filename*=UTF-8\'\'document.pdf', }); ``` ### Request * `file_id`: The unique identifier of the PDF file (required, path parameter) * `format`: The desired output format (required). Options: * `text`: Plain text output * `markdown`: Formatted markdown output ### Response Returns an object with a `content` field containing the extracted text in the specified format. ### Example code Once you have a file ID, here's how to parse the PDF content: ```bash cURL curl --location 'https://api.writer.com/v1/tools/pdf-parser/' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer WRITER_API_KEY' \ --data '{ "format": "markdown" }' ``` ```python Python import os from writerai import Writer client = Writer( # This is the default and can be omitted api_key=os.environ.get("WRITER_API_KEY"), ) response = client.tools.parse_pdf( file_id="file_id", format="text", ) print(response.content) ``` ```javascript JavaScript import Writer from 'writer-sdk'; const client = new Writer({ apiKey: process.env['WRITER_API_KEY'], // This is the default and can be omitted }); async function main() { const response = await client.tools.parsePdf('file_id', { format: 'text' }); console.log(response.content); } main(); ``` You now know how to use the key endpoints available in Writer's Tools API. For more details, refer to the [API reference](../api-guides/api-reference/tool-api/). # Get your organization subscription details get /billing/subscription We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them. # Detect If the content is AI generated post /content/organization/{organizationId}/detect We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them. # null post /content/organization/{organizationId}/team/{teamId}/check We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them. # null post /content/organization/{organizationId}/team/{teamId}/correct We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them. # null get /cowrite/organization/{organizationId}/team/{teamId}/template/{templateId} We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them. # null post /cowrite/organization/{organizationId}/team/{teamId}/generate We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them. # Delete snippets delete /snippet/organization/{organizationId}/team/{teamId} We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them. # Find snippets get /snippet/organization/{organizationId}/team/{teamId} We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them. # Update snippets put /snippet/organization/{organizationId}/team/{teamId} We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them. # List your styleguide pages get /styleguide/page We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them. # Page details get /styleguide/page/{pageId} We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them. # Add terms post /terminology/organization/{organizationId}/team/{teamId} We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them. # Delete terms delete /terminology/organization/{organizationId}/team/{teamId} We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them. # Find terms get /terminology/organization/{organizationId}/team/{teamId} We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them. # Update terms put /terminology/organization/{organizationId}/team/{teamId} We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them. # List users get /user We're deprecating these API endpoints, so the AI Studio API key setup won't work. \[Contact support] ([https://support.writer.com/](https://support.writer.com/)) if you need them. # Annotated text Shows text with annotations ## Fields
Name Type Description Options
Annotated text Object Value array with text/annotations. Must be a JSON string or a state reference to an array.
Reference Color The colour to be used as reference for chroma and luminance, and as the starting point for hue rotation.
Seed value Number Choose a different value to reshuffle colours.
Rotate hue Text If active, rotates the hue depending on the content of the string. If turned off, the reference colour is always used.
  1. yes
  2. no
Copy buttons Text If active, adds a control bar with both copy text and JSON buttons.
  1. yes
  2. no
Button Color
Button text Color
Primary text Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.Annotatedtext(content={ "text": {}, # Union[Dict, str] "referenceColor": "", # str "seed": 0.0, # Union[float, str] "rotateHue": "yes", # str [yes, no] "copyButtons": "no", # str [yes, no] "buttonColor": "", # str "buttonTextColor": "", # str "primaryTextColor": "", # str "cssClasses": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # Avatar A component to display user avatars. ## Fields
Name Type Description Options
Name Text
Image source Text A valid URL. Alternatively, you can provide a state reference to a packed file.
Caption Text Add an optional caption under the name, such as the person's job title.
Size Text
  1. Small
  2. Medium
  3. Large
Orientation Text
  1. Horizontal
  2. Vertical
Primary text Color
Secondary text Color
Separator Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Triggered when the avatar is clicked. ```python def handle_avatar_click(): print("The avatar was clicked") ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.Avatar(content={ "name": "", # str "imageSrc": "", # str "caption": "", # str "size": "medium", # str [small, medium, large] "orientation": "horizontal", # str [horizontal, vertical] "primaryTextColor": "", # str "secondaryTextColor": "", # str "separatorColor": "", # str "cssClasses": "", # str }, handlers={ "wf-click": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Button A standalone button component that can be linked to a click event handler. Writer Framework uses Material Symbols to display icons. To include an icon, check [https://fonts.google.com/icons](https://fonts.google.com/icons), find the icon's id (such as `arrow_forward`) and it to your *Button*. ## Fields
Name Type Description Options
Text Text
Disabled Text Disables all event handlers.
  1. Yes
  2. No
Button Color
Button text Color
Icon Text A Material Symbols id, such as "arrow\_forward".
Button shadow Shadow
Separator Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Capture single clicks. ```python def handle_button_click(state): # Increment counter when the button is clicked state["counter"] += 1 ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.Button(content={ "text": "", # str "isDisabled": "no", # str [yes, no] "buttonColor": "", # str "buttonTextColor": "", # str "icon": "", # str "buttonShadow": "", # str "separatorColor": "", # str "cssClasses": "", # str }, handlers={ "wf-click": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Chatbot A chatbot component to build human-to-AI interactions. Connect it to an LLM by handling the `wf-chatbot-message` event, which is triggered every time the user sends a message. You can add `actions` to messages, which are buttons that trigger the `wf-chatbot-action-click`. See the stubs for more details. ## Fields
Name Type Description Options
Conversation Object An array with messages or a writer.ai.Conversation object.
Assistant initials Text
User initials Text
Use Markdown Text If active, the output will be sanitized; unsafe elements will be removed.
  1. Yes
  2. No
Enable file upload Text
  1. Single file
  2. Multiple files
  3. No
Placeholder Text
Assistant role Color
User role Color
Avatar Color
Avatar text Color
Accent Color
Container background Color
Primary text Color
Secondary text Color
Separator Color
Button Color
Button text Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Triggered when the user sends a message. ```python def handle_message_simple(payload, state): # payload contains a dict in the form { "role": "user", "message": "hello"} state["conversation"] += [payload] state["conversation"] += [{ "role": "assistant", "content": "Hello human" if payload == "Hello" else "I don't understand" }] # Handle streaming by appending to the last message import time for i in range(10): conv = state["conversation"] conv[-1]["content"] += f" {i}" state["conversation"] = conv time.sleep(0.5) ``` Handle clicks on actions. ```python def handle_action_simple(payload, state): # payload contains the "data" property of the action if payload == "change_title": state["app_background_color"] = "red" # Make an action available when adding a message def handle_message_with_action(payload, state): state["conversation"] += [payload] state["conversation"] += [{ "role": "assistant", "content": "I don't know, but check this out.", "actions": [{ "subheading": "Resource", "name": "Surprise", "desc": "Click to be surprised", "data": "change_title" }] }] ``` Triggered when files are uploaded ```python def handle_file_upload(state, payload): # An array of dictionaries is provided in the payload # The dictionaries have the properties name, type and data # The data property is a file-like object uploaded_files = payload for i, uploaded_file in enumerate(uploaded_files): name = uploaded_file.get("name") file_data = uploaded_file.get("data") with open(f"{name}-{i}.jpeg", "wb") as file_handle: file_handle.write(file_data) ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.Chatbot(content={ "conversation": {}, # Union[Dict, str] "assistantInitials": "", # str "userInitials": "", # str "useMarkdown": "no", # str [yes, no] "enableFileUpload": "no", # str [single, multiple, no] "placeholder": "", # str "assistantRoleColor": "", # str "userRoleColor": "", # str "avatarBackgroundColor": "", # str "avatarTextColor": "", # str "accentColor": "", # str "containerBackgroundColor": "", # str "primaryTextColor": "", # str "secondaryTextColor": "", # str "separatorColor": "", # str "buttonColor": "", # str "buttonTextColor": "", # str "cssClasses": "", # str }, handlers={ "wf-chatbot-message": handle_event, "wf-chatbot-action-click": handle_event, "wf-file-change": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Checkbox Input A user input component that allows users to choose multiple values from a list of options using checkboxes. ## Fields
Name Type Description Options
Label Text
Options Key-Value Key-value object with options. Must be a JSON string or a state reference to a dictionary.
Orientation Text Specify how to lay out the options.
  1. Vertical
  2. Horizontal
Primary text Color
Accent Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Sent when the selected options change. ```python def onchange_handler(state, payload): # Set the state variable "selected" to the selected options. # The payload will be a list, as multiple options are allowed. state["selected"] = payload ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.CheckboxInput(content={ "label": "", # str "options": {}, # Union[Dict, str] "orientation": "vertical", # str [vertical, horizontal] "primaryTextColor": "", # str "accentColor": "", # str "cssClasses": "", # str }, handlers={ "wf-options-change": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Color Input A user input component that allows users to select a color using a color picker interface. ## Fields
Name Type Description Options
Label Text
Color List Object List of predefined colors
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Capture changes as they happen. ```python def onchange_handler(state, payload): # Set the state variable "new_color" to the new value, provided as string. state["new_color"] = payload ``` Capture changes once this control has lost focus. ```python def onchange_handler(state, payload): # Set the state variable "new_color" to the new value, provided as string. state["new_color"] = payload ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.ColorInput(content={ "label": "", # str "colorList": {}, # Union[Dict, str] "cssClasses": "", # str }, handlers={ "wf-change": handle_event, "wf-change-finish": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Column A layout component that organizes its child components in columns. Must be inside a Column Container component. ## Fields
Name Type Description Options
Title Text
Width (factor) Number Relative size when compared to other columns in the same container. A column of width 2 will be double the width of one with width 1.
Sticky Text
  1. Yes
  2. No
Collapsible Text
  1. Yes
  2. No
Start collapsed Text Only applied when the column is collapsible.
  1. Yes
  2. No
Separator Color
Padding Padding
Content alignment (H) Align (H)
Content alignment (V) Align (V)
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.Column(content={ "title": "", # str "width": 0.0, # Union[float, str] "isSticky": "no", # str [yes, no] "isCollapsible": "no", # str [yes, no] "startCollapsed": "no", # str [yes, no] "separatorColor": "", # str "contentPadding": "", # str "contentHAlign": "", # str "contentVAlign": "", # str "cssClasses": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # Column Container Serves as container for Column components ## Fields
Name Type Description Options
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.ColumnContainer(content={ "cssClasses": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # DataFrame A component to display Pandas DataFrames. ## Fields
Name Type Description Options
Data Text Must be a state reference to a Pandas dataframe or PyArrow table. Alternatively, a URL for an Arrow IPC file.
Show index Text Shows the dataframe's index. If an Arrow table is used, shows the zero-based integer index.
  1. yes
  2. no
Enable search Text
  1. yes
  2. no
Enable download Text Allows the user to download the data as CSV.
  1. yes
  2. no
Use Markdown Text If active, the output will be sanitized; unsafe elements will be removed.
  1. yes
  2. no
Display row count Number Specifies how many rows to show simultaneously.
Wrap text Text Not wrapping text allows for an uniform grid, but may be inconvenient if your data contains longer text fields.
  1. yes
  2. no
Primary text Color
Secondary text Color
Separator Color
Background Color
Font style Text
  1. normal
  2. monospace
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.DataFrame(content={ "dataframe": "", # str "showIndex": "yes", # str [yes, no] "enableSearch": "no", # str [yes, no] "enableDownload": "no", # str [yes, no] "useMarkdown": "no", # str [yes, no] "displayRowCount": 0.0, # Union[float, str] "wrapText": "no", # str [yes, no] "primaryTextColor": "", # str "secondaryTextColor": "", # str "separatorColor": "", # str "dataframeBackgroundColor": "", # str "fontStyle": "normal", # str [normal, monospace] "cssClasses": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # Date Input A user input component that allows users to select a date using a date picker interface. ## Fields
Name Type Description Options
Label Text
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Capture changes to this control. ```python def onchange_handler(state, payload): # Set the state variable "new_date" to the new value, provided as a YYYY-MM-DD string. state["new_date"] = payload ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.DateInput(content={ "label": "", # str "cssClasses": "", # str }, handlers={ "wf-date-change": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Dropdown Input A user input component that allows users to select a single value from a list of options using a dropdown menu. ## Fields
Name Type Description Options
Label Text
Options Key-Value Key-value object with options. Must be a JSON string or a state reference to a dictionary.
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Sent when the selected option changes. ```python def onchange_handler(state, payload): # Set the state variable "selected" to the selected option state["selected"] = payload ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.DropdownInput(content={ "label": "", # str "options": {}, # Union[Dict, str] "cssClasses": "", # str }, handlers={ "wf-option-change": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # File Input A user input component that allows users to upload files. ## Fields
Name Type Description Options
Label Text
Allowed file types Text Provides hints for browsers to select the correct file types. You can specify extensions and MIME types separated by comma, or leave empty to accept any file.
Allow multiple files Text
  1. Yes
  2. No
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Capture changes to this control. ```python def onchange_handler(state, payload): # An array of dictionaries is provided in the payload # The dictionaries have the properties name, type and data # The data property is a file-like object uploaded_files = payload for i, uploaded_file in enumerate(uploaded_files): name = uploaded_file.get("name") file_data = uploaded_file.get("data") with open(f"{name}-{i}.jpeg", "wb") as file_handle: file_handle.write(file_data) ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.FileInput(content={ "label": "", # str "allowFileTypes": "", # str "allowMultipleFiles": "no", # str [yes, no] "cssClasses": "", # str }, handlers={ "wf-file-change": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Google Maps A component to embed a Google Map. It can be used to display a map with markers. ## Fields
Name Type Description Options
API Key Text API Key from Google Cloud Console.
Map ID Text ID of map from Google Cloud Console, needed for markers.
Map type Text One of 'roadmap', 'satellite', 'hybrid' or 'terrain'.
  1. Roadmap
  2. Satellite
  3. Hybrid
  4. Terrain
Zoom Number
Latitude Number
Longitude Number
Markers Object Markers data
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Capture single clicks on markers. ```python ``` Capture single click on map. ```python ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.GoogleMaps(content={ "apiKey": "", # str "mapId": "", # str "mapType": "roadmap", # str [roadmap, satellite, hybrid, terrain] "zoom": 0.0, # Union[float, str] "lat": 0.0, # Union[float, str] "lng": 0.0, # Union[float, str] "markers": {}, # Union[Dict, str] "cssClasses": "", # str }, handlers={ "gmap-marker-click": handle_event, "gmap-click": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Header A container component that typically contains the main navigation elements. ## Fields
Name Type Description Options
Text Text
Primary text Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.Header(content={ "text": "", # str "primaryTextColor": "", # str "cssClasses": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # Heading A text component used to display headings or titles in different sizes and styles. ## Fields
Name Type Description Options
Text Text Add text directly, or reference state elements with @{my_text}.
Heading type Text
  1. h1 (Big)
  2. h2 (Normal)
  3. h3 (Small)
  4. h4 (Smallest)
Alignment Text
  1. Left
  2. Center
  3. Right
Primary text Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.Heading(content={ "text": "", # str "headingType": "h3", # str [h1, h2, h3, h4] "alignment": "left", # str [left, center, right] "primaryTextColor": "", # str "cssClasses": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # Horizontal Stack A layout component that stacks its child components horizontally, wrapping them to the next row if necessary. ## Fields
Name Type Description Options
Padding Padding
Content alignment (H) Align (H)
Content alignment (V) Align (V)
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.HorizontalStack(content={ "contentPadding": "", # str "contentHAlign": "", # str "contentVAlign": "", # str "cssClasses": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # HTML Element A generic component that creates customisable HTML elements, which can serve as containers for other components. You can configure the element type, styles, and attributes to fit your design requirements. You can link them to state for advanced use cases, such as custom animations. All valid HTML tags are supported, including tags such as `iframe`, allowing you to embed external sites. Take into account the potential risks of adding custom HTML to your app, including XSS. Be specially careful when injecting user-generated data. ## Fields
Name Type Description Options
Element Text Set the type of HTML element to create, e.g., 'div', 'section', 'span', etc.
Styles Object Define the CSS styles to apply to the HTML element using a JSON object or a state reference to a dictionary.
Attributes Object Set additional HTML attributes for the element using a JSON object or a dictionary via a state reference.
HTML inside Text Define custom HTML to be used inside the element. It will be wrapped in a div and rendered after children components.
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.HTMLElement(content={ "element": "", # str "styles": {}, # Union[Dict, str] "attrs": {}, # Union[Dict, str] "htmlInside": "", # str "cssClasses": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # Icon A component to display an icon Writer Framework uses Material Symbols to display icons. To include an icon, check [https://fonts.google.com/icons](https://fonts.google.com/icons), find the icon's id (such as `arrow_forward`) and it to your \_Icon. ## Fields
Name Type Description Options
Icon Text A Material Symbols id, such as "arrow\_forward".
Icon size Number Icon size in px
Icon color Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.Icon(content={ "icon": "", # str "size": 0.0, # Union[float, str] "color": "", # str "cssClasses": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # IFrame A component to embed an external resource in an iframe. ## Fields
Name Type Description Options
Source Text A valid URL
Separator Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Fires when the resource has successfully loaded. ```python def load_handler(state): # Sets status message when resource is loaded state["status"] = "Page loaded" ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.IFrame(content={ "src": "", # str "separatorColor": "", # str "cssClasses": "", # str }, handlers={ "wf-load": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Image A component to display images. Use your app's static folder to serve images directly. For example, `static/my_image.png`. Alternatively, pass a Matplotlib figure via state. `state["my_fig"] = fig` and then setting the *Image* source to `@{fig}` You can also use packed files or bytes: `state["img_b"] = wf.pack_bytes(img_bytes, "image/png")` `state["img_f"] = wf.pack_file(img_file, "image/png")` ## Fields
Name Type Description Options
Source Text A valid URL. Alternatively, you can provide a state reference to a Matplotlib figure or a packed file.
Caption Text Leave blank to hide.
Max width (px) Number
Max height (px) Number
Secondary text Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Capture single clicks. ```python def click_handler(state): # Increment counter when the image is clicked state["counter"] += 1 ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.Image(content={ "src": "", # str "caption": "", # str "maxWidth": 0.0, # Union[float, str] "maxHeight": 0.0, # Union[float, str] "secondaryTextColor": "", # str "cssClasses": "", # str }, handlers={ "wf-click": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # JSON Viewer A component to explore JSON data as a hierarchy. ## Fields
Name Type Description Options
Data Object
Initial depth Number Sets the initial viewing depth of the JSON tree hierarchy. Use -1 to display the full hierarchy.
Hide root Text Don't show the type of the root node when it's an Object or an Array.
  1. yes
  2. no
Copy Text If active, adds a control bar with copy JSON button.
  1. yes
  2. no
JSON indentation Width
Accent Color
Secondary text Color
Separator Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.JSONViewer(content={ "data": {}, # Union[Dict, str] "initialDepth": 0.0, # Union[float, str] "hideRoot": "no", # str [yes, no] "copy": "no", # str [yes, no] "jsonViewerIndentationSpacing": "", # str "accentColor": "", # str "secondaryTextColor": "", # str "separatorColor": "", # str "cssClasses": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # Link A component to create a hyperlink. ## Fields
Name Type Description Options
URL Text Specify a URL or choose a page. Keep in mind that you can only link to pages for which a key has been specified.
Target Text Specifies where to open the linked document.
  1. Self
  2. Blank
  3. Parent
  4. Top
Rel Text Specifies the relationship between the current document and the linked document.
Text Text The text to display in the link.
Primary text Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.Link(content={ "url": "", # str "target": "_self", # str [_self, _blank, _parent, _top] "rel": "", # str "text": "", # str "primaryTextColor": "", # str "cssClasses": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # Mapbox A component to embed a Mapbox map. It can be used to display a map with markers. For this component you need Mapbox access token: [https://www.mapbox.com/api-documentation/#access-tokens-and-token-scopes](https://www.mapbox.com/api-documentation/#access-tokens-and-token-scopes) ## Fields
Name Type Description Options
Access Token Text Access token from Mapbox
Map style Text Map style URL
Zoom Number
Latitude Number
Longitude Number
Markers Object
Controls visible Text Show map controls
  1. Yes
  2. No
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Capture single clicks on markers. ```python ``` Capture single click on map. ```python ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.Mapbox(content={ "accessToken": "", # str "mapStyle": "", # str "zoom": 0.0, # Union[float, str] "lat": 0.0, # Union[float, str] "lng": 0.0, # Union[float, str] "markers": {}, # Union[Dict, str] "controls": "yes", # str [yes, no] "cssClasses": "", # str }, handlers={ "mapbox-marker-click": handle_event, "mapbox-click": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Message A component that displays a message in various styles, including success, error, warning, and informational. When working with operations that can succeed or fail, *Message* can be useful. You can reserve a state element to be used for the outcome of the operation; empty messages aren't shown, so you can initialise it empty. Then, assign a message when the operation is completed. ```python state["msg"] = "" if is_ok: state["msg"] = "+It worked!" else: state["msg"] = "-It failed" ``` ## Fields
Name Type Description Options
Message Text Prefix with '+' for a success message, with '-' for error, '!' for warning, '%' for loading. No prefix for info. Leave empty to hide.
Success Color
Error Color
Warning Color
Info Color
Loading Color
Primary text Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.Message(content={ "message": "", # str "successColor": "", # str "errorColor": "", # str "warningColor": "", # str "infoColor": "", # str "loadingColor": "", # str "primaryTextColor": "", # str "cssClasses": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # Metric A component that prominently displays a metric value and associated information. ## Fields
Name Type Description Options
Name Text
Value Text The main value to be displayed. It's not limited to numbers.
Description Text
Note Text Prefix with '+' for a positive message, with '-' for a negative message.
Primary text Color
Secondary text Color
Positive Color
Neutral Color
Negative Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.Metric(content={ "name": "", # str "metricValue": "", # str "description": "", # str "note": "", # str "primaryTextColor": "", # str "secondaryTextColor": "", # str "positiveColor": "", # str "neutralColor": "", # str "negativeColor": "", # str "cssClasses": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # Multiselect Input A user input component that allows users to select multiple values from a searchable list of options. ## Fields
Name Type Description Options
Label Text
Options Key-Value Key-value object with options. Must be a JSON string or a state reference to a dictionary.
Placeholder Text Text to show when no options are selected.
Maximum count Number The maximum allowable number of selected options. Set to zero for unlimited.
Accent Color The colour of the chips created for each selected option.
Chip text Color The colour of the text in the chips.
Primary text Color
Secondary text Color
Container background Color
Separator Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Sent when the selected options change. ```python def onchange_handler(state, payload): # Set the state variable "selected" to the selected option state["selected"] = payload ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.MultiselectInput(content={ "label": "", # str "options": {}, # Union[Dict, str] "placeholder": "", # str "maximumCount": 0.0, # Union[float, str] "accentColor": "", # str "chipTextColor": "", # str "primaryTextColor": "", # str "secondaryTextColor": "", # str "containerBackgroundColor": "", # str "separatorColor": "", # str "cssClasses": "", # str }, handlers={ "wf-options-change": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Number Input A user input component that allows users to enter numeric values. ## Fields
Name Type Description Options
Label Text
Placeholder Text
Minimum value Number
Max value Number
Step Number
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Capture changes as they happen. ```python def onchange_handler(state, payload): # Set the state variable "new_val" to the new value state["new_val"] = payload ``` Capture changes once this control has lost focus. ```python def onchange_handler(state, payload): # Set the state variable "new_val" to the new value state["new_val"] = payload ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.NumberInput(content={ "label": "", # str "placeholder": "", # str "minValue": 0.0, # Union[float, str] "maxValue": 0.0, # Union[float, str] "valueStep": 0.0, # Union[float, str] "cssClasses": "", # str }, handlers={ "wf-number-change": handle_event, "wf-number-change-finish": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Pagination A component that can help you paginate records, for example from a Repeater or a DataFrame. ## Fields
Name Type Description Options
Page Number The current page number.
Page Size Number The number of items per page.
Total Items Number The total number of items
Page Size Options Text A comma-separated list of page size options. If it's empty, the user can't change the page size. Set your default page size as the first option.
Show All Option Text Show an option to show all records.
  1. Yes
  2. No
Jump To Text Show an option to jump to a specific page.
  1. Yes
  2. No
## Events Fires when the user pick a page ```python def handle_page_change(state, payload): page = payload state["page"] = page records = _load_records_from_db(start = state["page"] * state["pageSize"], limit = state["pageSize"]) # update a repeater state["highlighted_members"] = {r.id: r for r in records} ``` Fires when the user change the page size. ```python def handle_page_size_change(state, payload): state['pageSize'] = payload records = _load_records_from_db(start = state["page"] * state["pageSize"], limit = state["pageSize"]) # update a repeater state["highlighted_members"] = {r.id: r for r in records} ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.Pagination(content={ "page": 0.0, # Union[float, str] "pageSize": 0.0, # Union[float, str] "totalItems": 0.0, # Union[float, str] "pageSizeOptions": "", # str "pageSizeShowAll": "no", # str [yes, no] "jumpTo": "no", # str [yes, no] }, handlers={ "wf-change-page": handle_event, "wf-change-page-size": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # PDF A component to embed PDF documents. ## Fields
Name Type Description Options
PDF source Text A valid URL. Alternatively, you can provide a state reference to a packed PDF file.
Highlights Object A list of highlights to be applied to the PDF as a JSON array of strings.
Selected highlight match Number The index of the selected highlight match.
Page Number The page to be displayed.
Controls Text Show controls to navigate the PDF.
  1. Yes
  2. No
Container background Color
Separator Color
Primary text Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.PDF(content={ "source": "", # str "highlights": {}, # Union[Dict, str] "selectedMatch": 0.0, # Union[float, str] "page": 0.0, # Union[float, str] "controls": "yes", # str [yes, no] "containerBackgroundColor": "", # str "separatorColor": "", # str "primaryTextColor": "", # str "cssClasses": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # Plotly Graph A component that displays Plotly graphs. You can listen to events triggered by Plotly.js and add interactivity to your charts. For example, implement cross-filtering. ## Fields
Name Type Description Options
Graph specification Object Plotly graph specification. Pass it using state, e.g. @{fig}, or paste a JSON specification.
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Sends a list with the clicked points. ```python ``` Sends a list with the selected points. ```python ``` Triggered when points are deselected. ```python ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.PlotlyGraph(content={ "spec": {}, # Union[Dict, str] "cssClasses": "", # str }, handlers={ "plotly-click": handle_event, "plotly-selected": handle_event, "plotly-deselect": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Radio Input A user input component that allows users to choose a single value from a list of options using radio buttons. ## Fields
Name Type Description Options
Label Text
Options Key-Value Key-value object with options. Must be a JSON string or a state reference to a dictionary.
Orientation Text Specify how to lay out the options.
  1. Vertical
  2. Horizontal
Primary text Color
Accent Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Sent when the selected option changes. ```python def onchange_handler(state, payload): # Set the state variable "selected" to the selected radio option state["selected"] = payload ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.RadioInput(content={ "label": "", # str "options": {}, # Union[Dict, str] "orientation": "vertical", # str [vertical, horizontal] "primaryTextColor": "", # str "accentColor": "", # str "cssClasses": "", # str }, handlers={ "wf-option-change": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Slider Range Input A user input component that allows users to select numeric values range using a range slider with optional constraints like min, max, and step. ## Fields
Name Type Description Options
Label Text
Minimum value Number
Maximum value Number
Step size Number
Accent Color
Popover color Color
Popover background Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Capture changes to this control. ```python def onchange_handler(state, payload): # Set the state variables "from" & "to" to the new range state["from"] = payload[0] state["to"] = payload[1] ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.SliderRangeInput(content={ "label": "", # str "minValue": 0.0, # Union[float, str] "maxValue": 0.0, # Union[float, str] "stepSize": 0.0, # Union[float, str] "accentColor": "", # str "popoverColor": "", # str "popoverBackgroundColor": "", # str "cssClasses": "", # str }, handlers={ "wf-range-change": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Rating Input A user input component that allows users to provide a rating. ## Fields
Name Type Description Options
Label Text
Feedback Text
  1. Stars
  2. Faces
  3. Hearts
Minimum value Number Valid values are 0 and 1.
Max value Number Valid values are between 2 and 11.
Step Number Valid values are between 0.25 and 1.
Accent Color
Primary text Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events ```python def onchange_handler(state, payload): # Set the state variable "rating" to the new value state["rating"] = payload ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.RatingInput(content={ "label": "", # str "feedback": "stars", # str [stars, faces, hearts] "minValue": 0.0, # Union[float, str] "maxValue": 0.0, # Union[float, str] "valueStep": 0.0, # Union[float, str] "accentColor": "", # str "primaryTextColor": "", # str "cssClasses": "", # str }, handlers={ "wf-number-change": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Repeater A container component that repeats its child components based on a dictionary. ## Fields
Name Type Description Options
Repeater object Object Include a state reference to the dictionary used for repeating the child components. Alternatively, specify a JSON object.
Key variable name Text Set the name of the variable that will store the key of the current repeater object entry.
Value variable name Text Set the name of the variable that will store the value of the current repeater object entry.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.Repeater(content={ "repeaterObject": {}, # Union[Dict, str] "keyVariable": "", # str "valueVariable": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # Reuse Component Those components are used to reuse other components. Reused components share the same state and are updated together. ## Fields
Name Type Description Options
Component id Text The id of the component to reuse.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.ReuseComponent(content={ "proxyId": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # Section A container component that divides the layout into sections, with an optional title. ## Fields
Name Type Description Options
Title Text Leave blank to hide.
Collapsible Text
  1. Yes
  2. No
Start collapsed Text Only applied when the component is collapsible.
  1. Yes
  2. No
Accent Color
Primary text Color
Secondary text Color
Container background Color
Container shadow Shadow
Separator Color
Button Color
Button text Color
Button shadow Shadow
Padding Padding
Content alignment (H) Align (H)
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.Section(content={ "title": "", # str "isCollapsible": "no", # str [yes, no] "startCollapsed": "no", # str [yes, no] "accentColor": "", # str "primaryTextColor": "", # str "secondaryTextColor": "", # str "containerBackgroundColor": "", # str "containerShadow": "", # str "separatorColor": "", # str "buttonColor": "", # str "buttonTextColor": "", # str "buttonShadow": "", # str "contentPadding": "", # str "contentHAlign": "", # str "cssClasses": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # Select Input A user input component that allows users to select a single value from a searchable list of options. ## Fields
Name Type Description Options
Label Text
Options Key-Value Key-value object with options. Must be a JSON string or a state reference to a dictionary.
Placeholder Text Text to show when no options are selected.
Maximum count Number The maximum allowable number of selected options. Set to zero for unlimited.
Accent Color
Chip text Color The color of the text in the chips.
Primary text Color
Secondary text Color
Container background Color
Separator Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Sent when the selected option changes. ```python def onchange_handler(state, payload): # Set the state variable "selected" to the selected option state["selected"] = payload ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.SelectInput(content={ "label": "", # str "options": {}, # Union[Dict, str] "placeholder": "", # str "maximumCount": 0.0, # Union[float, str] "accentColor": "", # str "chipTextColor": "", # str "primaryTextColor": "", # str "secondaryTextColor": "", # str "containerBackgroundColor": "", # str "separatorColor": "", # str "cssClasses": "", # str }, handlers={ "wf-option-change": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Separator A visual component to create a separation between adjacent elements. *Separator* components are used to separate layout elements. They can be used in most containers, including *Column Container* to separate columns. If the container flows horizontally (like a *Horizontal Stack* or a *Column Container*) the *Separator* will be a vertical line. Otherwise, it'll be a horizontal line. ## Fields
Name Type Description Options
Separator Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.Separator(content={ "separatorColor": "", # str "cssClasses": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # Sidebar A container component that organizes its children in a sidebar. Its parent must be a Page component. ## Fields
Name Type Description Options
Start collapsed Text
  1. Yes
  2. No
Background Color
Accent Color
Primary text Color
Secondary text Color
Container background Color
Container shadow Shadow
Separator Color
Button Color
Button text Color
Button shadow Shadow
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.Sidebar(content={ "startCollapsed": "no", # str [yes, no] "sidebarBackgroundColor": "", # str "accentColor": "", # str "primaryTextColor": "", # str "secondaryTextColor": "", # str "containerBackgroundColor": "", # str "containerShadow": "", # str "separatorColor": "", # str "buttonColor": "", # str "buttonTextColor": "", # str "buttonShadow": "", # str "cssClasses": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # Slider Input A user input component that allows users to select numeric values using a slider with optional constraints like min, max, and step. ## Fields
Name Type Description Options
Label Text
Minimum value Number
Maximum value Number
Step size Number
Accent Color
Popover color Color
Popover background Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Capture changes to this control. ```python def onchange_handler(state, payload): # Set the state variable "new_val" to the new value state["new_val"] = payload ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.SliderInput(content={ "label": "", # str "minValue": 0.0, # Union[float, str] "maxValue": 0.0, # Union[float, str] "stepSize": 0.0, # Union[float, str] "accentColor": "", # str "popoverColor": "", # str "popoverBackgroundColor": "", # str "cssClasses": "", # str }, handlers={ "wf-number-change": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Step A container component that displays its child components as a step inside a Step Container. ## Fields
Name Type Description Options
Name Text
Padding Padding
Completed Text Use a state reference to dynamically mark this step as complete.
  1. Yes
  2. No
Content alignment (H) Align (H)
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.Step(content={ "name": "", # str "contentPadding": "", # str "isCompleted": "no", # str [yes, no] "contentHAlign": "", # str "cssClasses": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # Step Container A container component for displaying Step components, allowing you to implement a stepped workflow. ## Fields
Name Type Description Options
Accent Color
Primary text Color
Secondary text Color
Container background Color
Container shadow Shadow
Separator Color
Button Color
Button text Color
Button shadow Shadow
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.StepContainer(content={ "accentColor": "", # str "primaryTextColor": "", # str "secondaryTextColor": "", # str "containerBackgroundColor": "", # str "containerShadow": "", # str "separatorColor": "", # str "buttonColor": "", # str "buttonTextColor": "", # str "buttonShadow": "", # str "cssClasses": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # Switch Input A user input component with a simple on/off status. ## Fields
Name Type Description Options
Label Text
Accent Color
Primary text Color
Separator Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Sent when the switch is toggled. ```python def handle_toggle(state, payload): # The payload will be a bool state["its_on"] = payload ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.SwitchInput(content={ "label": "", # str "accentColor": "", # str "primaryTextColor": "", # str "separatorColor": "", # str "cssClasses": "", # str }, handlers={ "wf-toggle": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Tab A container component that displays its child components as a tab inside a Tab Container. ## Fields
Name Type Description Options
Name Text
Padding Padding
Content alignment (H) Align (H)
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.Tab(content={ "name": "", # str "contentPadding": "", # str "contentHAlign": "", # str "cssClasses": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # Tab Container A container component for organising and displaying Tab components in a tabbed interface. ## Fields
Name Type Description Options
Accent Color
Primary text Color
Secondary text Color
Container background Color
Container shadow Shadow
Separator Color
Button Color
Button text Color
Button shadow Shadow
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.TabContainer(content={ "accentColor": "", # str "primaryTextColor": "", # str "secondaryTextColor": "", # str "containerBackgroundColor": "", # str "containerShadow": "", # str "separatorColor": "", # str "buttonColor": "", # str "buttonTextColor": "", # str "buttonShadow": "", # str "cssClasses": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # Tags A component to display coloured tag pills. ## Fields
Name Type Description Options
Tags Key-Value Key-value object with tags. Must be a JSON string or a state reference to a dictionary.
Reference Color The colour to be used as reference for chroma and luminance, and as the starting point for hue rotation.
Seed value Number Choose a different value to reshuffle colours.
Rotate hue Text If active, rotates the hue depending on the content of the string. If turned off, the reference colour is always used.
  1. yes
  2. no
Primary text Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Triggered when a tag is clicked. ```python def handle_tag_click(state, payload): state["selected_tag_id"] = payload ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.Tags(content={ "tags": {}, # Union[Dict, str] "referenceColor": "", # str "seed": 0.0, # Union[float, str] "rotateHue": "yes", # str [yes, no] "primaryTextColor": "", # str "cssClasses": "", # str }, handlers={ "wf-tag-click": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Text A component to display plain text or formatted text using Markdown syntax. ## Fields
Name Type Description Options
Text Text Add text directly, or reference state elements with @{my_text}.
Use Markdown Text The Markdown output will be sanitised; unsafe elements will be removed.
  1. Yes
  2. No
Alignment Text
  1. Left
  2. Center
  3. Right
Primary text Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Capture single clicks. ```python def click_handler(state): # Increment counter when the text is clicked state["counter"] += 1 ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.Text(content={ "text": "", # str "useMarkdown": "no", # str [yes, no] "alignment": "left", # str [left, center, right] "primaryTextColor": "", # str "cssClasses": "", # str }, handlers={ "wf-click": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Textarea Input A user input component that allows users to enter multi-line text values. ## Fields
Name Type Description Options
Label Text
Placeholder Text
Rows Number
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Capture changes as they happen. ```python def onchange_handler(state, payload): # Set the state variable "new_val" to the new value state["new_val"] = payload ``` Capture changes once this control has lost focus. ```python def onchange_handler(state, payload): # Set the state variable "new_val" to the new value state["new_val"] = payload ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.TextareaInput(content={ "label": "", # str "placeholder": "", # str "rows": 0.0, # Union[float, str] "cssClasses": "", # str }, handlers={ "wf-change": handle_event, "wf-change-finish": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Text Input A user input component that allows users to enter single-line text values. ## Fields
Name Type Description Options
Label Text
Placeholder Text
Password mode Text
  1. Yes
  2. No
Accent Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Capture changes as they happen. ```python def onchange_handler(state, payload): # Set the state variable "new_val" to the new value state["new_val"] = payload ``` Capture changes once this control has lost focus. ```python def onchange_handler(state, payload): # Set the state variable "new_val" to the new value state["new_val"] = payload ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.TextInput(content={ "label": "", # str "placeholder": "", # str "passwordMode": "no", # str [yes, no] "accentColor": "", # str "cssClasses": "", # str }, handlers={ "wf-change": handle_event, "wf-change-finish": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Time Input A user input component that allows users to select a time. ## Fields
Name Type Description Options
Label Text
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Capture changes to this control. ```python def onchange_handler(state, payload): # Set the state variable "new_time" to the new value, provided as a hh:mm string (in 24-hour format that includes leading zeros). state["new_time"] = payload ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.TimeInput(content={ "label": "", # str "cssClasses": "", # str }, handlers={ "wf-time-change": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Timer A component that emits an event repeatedly at specified time intervals, enabling time-based refresh. ## Fields
Name Type Description Options
Interval (ms) Number How much time to wait between ticks. A tick is considered finished when its event is handled.
Active Text Whether the timer should trigger tick events.
  1. Yes
  2. No
Accent Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Emitted when the timer ticks. ```python def handle_timer_tick(state): # Increment counter when the timer ticks state["counter"] += 1 ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.Timer(content={ "intervalMs": 0.0, # Union[float, str] "isActive": "yes", # str [yes, no] "accentColor": "", # str "cssClasses": "", # str }, handlers={ "wf-tick": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Vega Lite Chart A component that displays Vega-Lite/Altair charts. Generate a chart using Altair and pass it via state; it'll be converted to Vega-Lite specification. `state["my_chart"] = chart` Afterwards, you can reference the chart in the specification using the syntax `@{my_chart}`. Alternatively, you can work with Vega-Lite directly. ## Fields
Name Type Description Options
Chart specification Object Vega-Lite chart specification. Pass a Vega Altair chart using state or paste a JSON specification.
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.VegaLiteChart(content={ "spec": {}, # Union[Dict, str] "cssClasses": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # Video Player A video player component that can play various video formats. Use your app's static folder to serve videos directly. For example, `static/my_video.mp4`. Alternatively, you can pack bytes or files in state: `state["vid_b"] = wf.pack_bytes(vid_bytes, "video/mp4")` `state["vid_f"] = wf.pack_file(vid_file, "video/mp4")` Afterwards, you can reference the video using the syntax `@{vid_f}`. ## Fields
Name Type Description Options
Source Text The URL of the video file. Alternatively, you can pass a file via state.
Controls Text Display video player controls.
  1. Yes
  2. No
Autoplay Text Autoplay the video when the component is loaded.
  1. Yes
  2. No
Loop Text Loop the video when it reaches the end.
  1. Yes
  2. No
Muted Text Mute the video by default.
  1. Yes
  2. No
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.VideoPlayer(content={ "src": "", # str "controls": "yes", # str [yes, no] "autoplay": "no", # str [yes, no] "loop": "no", # str [yes, no] "muted": "no", # str [yes, no] "cssClasses": "", # str } ) ``` ## Reference * Explore this component's source code on GitHub # Webcam Capture A user input component that allows users to capture images using their webcam. ## Fields
Name Type Description Options
Refresh rate (ms) Number Set to 0 for manual capture.
Button Color
Button text Color
Button shadow Shadow
Separator Color
Custom CSS classes Text CSS classes, separated by spaces. You can define classes in custom stylesheets.
## Events Sent when a frame is captured. Its payload contains the captured frame in PNG format. ```python def webcam_handler(payload): # This handler will save the captured images based on timestamp import time timestamp = time.time() # The payload is a file-like object which contains the captured image # in PNG format image_file = payload with open(f"capture-{timestamp}.png", "wb") as file_handle: file_handle.write(image_file) ``` ## Low code usage This component can be declared directly in Python, using [backend-driven UI](../backend-driven-ui). ```python ui.WebcamCapture(content={ "refreshRate": 0.0, # Union[float, str] "buttonColor": "", # str "buttonTextColor": "", # str "buttonShadow": "", # str "separatorColor": "", # str "cssClasses": "", # str }, handlers={ "wf-webcam": handle_event, } ) ``` A function, in this example `handle_event`, should be implemented in your code to handle events. ```python def handle_event(state, payload, context, ui): pass ``` ## Reference * Explore this component's source code on GitHub # Writer AI module This module leverages the [Writer Python SDK](https://pypi.org/project/writer-sdk/) to enable applications to interact with large language models (LLMs) in chat or text completion formats. It provides tools to manage conversation states and to dynamically interact with LLMs using both synchronous and asynchronous methods. ## Getting your API key To utilize the Writer AI module, you'll need to configure the `WRITER_API_KEY` environment variable with an API key obtained from AI Studio. Here is a detailed [guide](/api-guides/quickstart) to setup up this key. You will need to select an **API** app under **Developer tools** Once you have your API key, set it as an environment variable on your system: ```bash For macOS and Linux export WRITER_API_KEY=your_api_key_here ``` ```bash For Windows set WRITER_API_KEY=your_api_key_here ``` You can manage your environment variables using methods that best suit your setup, such as employing tools like [python-dotenv](https://pypi.org/project/python-dotenv/). Furthermore, when deploying an application with `writer deploy`, the `WRITER_API_KEY` environment variable is automatically configured with the API key specified during the deployment process. ## Chat completion with the Conversation class The `Conversation` class manages LLM communications within a chat framework, storing the conversation history and handling the interactions. ```python import writer as wf import writer.ai def handle_simple_message(state, payload): # Update the conversation state by appending the incoming user message. state["conversation"] += payload # Stream the complete response from the AI model in chunks. for chunk in state["conversation"].stream_complete(): # Append each chunk of the model's response to the ongoing conversation state. state["conversation"] += chunk # Initialize the application state with a new Conversation object. initial_state = wf.init_state({ "conversation": writer.ai.Conversation(), }) ``` ### Initializing a conversation A `Conversation` can be initialized with either a system prompt or a list of previous messages. It can also accept a default configuration dictionary that sets parameters for all interactions. ```python # Initialize with a system prompt for a Financial Analyst specializing in balance sheets conversation = Conversation("You assist clients with analyzing and understanding their balance sheets") # Initialize with a history of messages related to balance sheet queries history = [ {"role": "user", "content": "Can you explain the liabilities section?"}, {"role": "assistant", "content": "Certainly! Liabilities are legally binding obligations payable to another entity."} ] conversation = Conversation(history) # Initialize with a configuration suitable for financial analysis discussions config = {'max_tokens': 200, 'temperature': 0.5} conversation = Conversation("You provide detailed insights into balance sheet components", config=config) ``` ### Adding messages to conversation Messages can be added to a `Conversation` instance using the `+` operator or the `add` method. ```python # Using the `+` operator to add a balance sheet-related query conversation += {"role": "user", "content": "Can you break down the assets section of the balance sheet?"} # Using the `add` method to add a balance sheet-related query conversation.add(role="user", content="How should I interpret the equity section?") ``` ### Completing and streaming Conversations The `complete` and `stream_complete` methods facilitate interaction with the LLM based on the accumulated messages and configuration. These methods execute calls to generate responses and return them in the form of a message object, but do not alter the conversation's `messages` list, allowing you to validate or modify the output before deciding to add it to the history. ```python complete # Using `complete` to get a single response response = conversation.complete() print("LLM Response:", response) ``` ```python stream_complete # Using `stream_complete` to get streamed responses for chunk in conversation.stream_complete(): print("Streamed Message:", chunk) # Manually adding to the conversation conversation += chunk ``` Instance-wide configuration parameters can be complemented or overriden on individual call's level, if a `config` dictionary is provided to the method: ```python # Overriding configuration for a specific call response = conversation.complete(config={'max_tokens': 200, 'temperature': 0.5}) ``` ### Using Graphs with Conversation A `Graph` is a collection of files meant to provide their contents to the LLM during conversations. Framework allows you to create, retrieve, update, and delete graphs, as well as manage the files within them. #### Creating and Managing Graphs To create and manipulate graphs, use the following methods: ```python from writer.ai import create_graph, retrieve_graph, list_graphs, delete_graph # Create a new graph graph = create_graph(name="Financial Data", description="Quarterly reports") # Retrieve an existing graph by ID graph = retrieve_graph("d90a632b-5c1f-42b8-8748-5b7f769d9a36") # Update a graph graph.update(name="Updated Financial Data", description="Updated description") # Retrieve a list of created graphs graphs = list_graphs() for graph in graphs: # Delete a graph delete_graph(graph) ``` #### Adding and Removing Files from Graphs You can upload files, associate them with graphs, and download or remove them. ```python from writer.ai import upload_file # Upload a file file = upload_file(data=b"file content", type="application/pdf", name="Report.pdf") # Add the file to a graph graph.add_file(file) # Remove the file from the graph graph.remove_file(file) ``` #### Applying Graphs to Conversation completion You can utilize graphs within conversations. For instance, you may want to provide the LLM access to a collection of files during an ongoing conversation to query or analyze the file content. When passing a graph to the conversation, the LLM can query the graph to retrieve relevant data. ```python # Retrieve a graph graph = retrieve_graph("d90a632b-5c1f-42b8-8748-5b7f769d9a36") # Pass the graph to the conversation for completion response = conversation.complete(tools=graph) ``` Alternatively, you can define a graph using JSON: ```python tool = { "type": "graph", "graph_ids": ["d90a632b-5c1f-42b8-8748-5b7f769d9a36"] } response = conversation.complete(tools=tool) ``` ### Using Function Calls with Conversations Function tools are only available with `palmyra-x-004` model Framework allows you to register Python functions that can be called automatically during conversations. When the LLM determines a need for specific information or processing, it issues a request to use the local code (your function), and Framework handles that request automatically. #### Defining Function Tools Function tools are defined using either a Python class or a JSON configuration. ```python from writer.ai import create_function_tool # Define a function tool with Python callable def calculate_interest(principal: float, rate: float, time: float): return principal * rate * time tool = create_function_tool( name="calculate_interest", callable=calculate_interest, parameters={ "principal": {"type": "float", "description": "Loan principal"}, "rate": {"type": "float", "description": "Interest rate"}, "time": {"type": "float", "description": "Time in years"} } ) response = conversation.complete(tools=tool) ``` Alternatively, you can define a function tool in JSON format, but the callable function must still be passed: ```python tool = { "type": "function", "name": "calculate_interest", "callable": calculate_interest, "parameters": { "principal": {"type": "float", "description": "Loan principal"}, "rate": {"type": "float", "description": "Interest rate"}, "time": {"type": "float", "description": "Time in years"} } } response = conversation.complete(tools=tool) ``` Function tools require the following properties: * **`name: str`**: A string that defines how the function is referenced by the LLM. It should describe the function’s purpose. * **`callable: Callable`**: A Python function that will be called automatically when needed by the LLM. * **`parameters: dict`**: A dictionary that specifies what input the function expects. The keys should match the function’s parameter names, and each parameter should have a `type`, and an optional `description`.\ Supported types are: `string`, `number`, `integer`, `float`, `boolean`, `array`, `object` and `null`. #### Automated Function Calling When a conversation involves a tool (either a graph or a function), Framework automatically handles the requests from LLM to use the tools during interactions. If the tool needs multiple steps (for example, querying data and processing it), Framework will handle those steps recursively, calling functions as needed until the final result is returned. By default, to prevent endless recursion, Framework will only handle 3 consecutive tool calls. You can expand it in case it doesn't suit your case – both `complete()` and `stream_complete()` accept a `max_tool_depth` parameter, which configures the maximum allowed recursion depth: ```python response = conversation.complete(tools=tool, max_tool_depth=7) ``` ### Providing a Tool or a List of Tools You can pass either a single tool or a list of tools to the `complete()` or `stream_complete()` methods. The tools can be a combination of FunctionTool, Graph, or JSON-defined tools. ```python from writer.ai import create_function_tool, retrieve_graph # Define a function tool tool1 = create_function_tool( name="get_data", callable=lambda x: f"Data for {x}", parameters={"x": {"type": "string", "description": "Input value"}} ) # Retrieve a graph graph = retrieve_graph("d90a632b-5c1f-42b8-8748-5b7f769d9a36") # Provide both tools in a list response = conversation.complete(tools=[tool1, graph]) ``` ## Text generation without a conversation state ### Text generation against a string prompt `complete` and `stream_complete` methods are designed for one-off text generation without the need to manage a conversation state. They return the model's response as a string. Each function accepts a `config` dictionary allowing call-specific configurations. ```python complete # Using `complete` for a single completion text_response = complete("Explore the benefits of AI.", config={'temperature': 0.3}) print("Completion:", text_response) ``` ```python stream_complete # Using `stream_complete` for streamed text completions for text_chunk in stream_complete("Explore the benefits of AI.", config={'temperature': 0.3}): print("Streamed Text:", text_chunk) ``` ### Text generation against graphs The `ask` and `stream_ask` methods allow you to query one or more graphs to generate responses from the information stored within them. #### Two approaches to questioning graphs There are two ways to query graphs, depending on your needs: 1. **Graph-Level Methods** (`Graph.ask`, `Graph.stream_ask`): Used when working with a single graph instance. These methods are tied directly to the Graph object, encapsulating operations within that instance. 2. **Module-Level Methods** (`writer.ai.ask`, `writer.ai.stream_ask`): Designed for querying multiple graphs simultaneously. These methods operate on a broader scale, allowing mixed inputs of graph objects and IDs. • Use graph-level methods when working with a single graph instance. • Use module-level methods when querying multiple graphs or when graph IDs are your primary input. #### Parameters Both methods include: • `question: str`: The main query for the LLM. • *Optional* `subqueries: bool` (default: `False`): Allows the LLM to generate additional questions during response preparation for more detailed answers. Enabling this might increase response time. Method-level methods require: • `graphs_or_graph_ids: list[Graph | str]`: A list of graphs to use for the question. You can pass `Graph` objects directly into the list, use graph IDs in string form, or a mix of both. #### Graph-level methods The graph-level methods, `Graph.ask` and `Graph.stream_ask`, are designed for interacting with a single graph. By calling these methods on a specific `Graph` instance, you can easily pose questions and retrieve answers tailored to that graph’s content. ```python ask # Retrieve a specific graph graph = retrieve_graph("f47ac10b-58cc-4372-a567-0e02b2c3d479") # Pose a question to the graph and get a complete response response = graph.ask("What are the benefits of renewable energy?") print(response) ``` ```python stream_ask # Retrieve a specific graph graph = retrieve_graph("f47ac10b-58cc-4372-a567-0e02b2c3d479") # Pose a question and stream the response in chunks for chunk in graph.stream_ask("Explain the history of solar energy."): print(chunk) ``` #### Module-level methods The module-level methods, `writer.ai.ask` and `writer.ai.stream_ask`, are designed for querying multiple graphs simultaneously. They are useful when you need to aggregate or compare data across multiple graphs. ```python ask from writer.ai import ask # Pose a question to multiple graphs response = ask( question="What are the latest advancements in AI?", graphs_or_graph_ids=[ "550e8400-e29b-41d4-a716-446655440000", "123e4567-e89b-12d3-a456-426614174000" ] ) print(response) ``` ```python stream_ask from writer.ai import stream_ask # Stream responses from multiple graphs for chunk in stream_ask( question="Describe the key features of renewable energy sources.", graphs_or_graph_ids=[ "550e8400-e29b-41d4-a716-446655440000", "123e4567-e89b-12d3-a456-426614174000" ] ): print(chunk) ``` # Application state Each session is assigned a unique application state by the Framework. ## Initializing state To set the initial application state, use the `wf.init_state()` method with a dictionary argument. All user sessions will start with a clone of this initial state. ```py import writer as wf # Define the initial state initial_state = wf.init_state({ "counter": 0, }) # Define an event handler that modifies the state # It receives the session state as an argument and mutates it def increment(state): state["counter"] += 1 ``` In the above example, each session begins with a `counter` at 0. As users interact with the application and activate event handlers, their session's `counter` value will change. For instance, if a user triggers the `increment` handler three times, their counter will increase to 3. To access the `counter` value in the Builder, use @{counter}. ### Managing nested state elements To include nested elements in your state, use nested dictionaries: ```python # Example of nested state initialization wf.init_state({ "counter": 0, "my_app": { "title": "Nested value" } }) ``` You can reference nested elements in the Builder as `@{my_app.title}`. ### Backend-only state elements By default, all of the elements in the session state are sent to the front-end. All state elements are transmitted to the front-end by default, regardless of their visibility in the user interface. To keep certain state elements private (back-end-only), prefix them with an underscore `_`. This is useful in several scenarios: 1. When data synchronization to the front-end is unnecessary. 2. When data cannot be serialized for the front-end, such as database connections. 3. When data is sensitive to the specific session and should remain confidential. These elements remain in the back-end and cannot be accessed from the Builder. ## Managing files and binary data In components where the Builder interfaces with external data, such as images, it often requires the use of data URLs. The source for an *Image* component, for example, can be a standard URL or a data URL. Packing Files and Binary Data: Files and binary data can be converted to data URLs before they are sent to the front-end. Use `wf.pack_file()` and `wf.pack_bytes()` for this purpose. The `mime_type` argument, while optional, specifies the media type, helping the browser to correctly handle the data. ```python import writer as wf # Initialize state with various data types wf.init_state({ # Reference a file by its filesystem path "sales_spreadsheet": wf.pack_file("sales_spreadsheet.xlsx"), # Use a file-like object that implements a .read() method "main_image": wf.pack_file(image_file, mime_type="image/jpeg"), # Convert raw bytes specifying a MIME type "my_bytes": wf.pack_bytes(b"\x31\x33\x33\x37", mime_type="text/plain"), # Directly assign raw bytes without a MIME type "my_raw_bytes": b"\x31\x33\x33\x37", }) ``` ## Handling non-standard data types The front-end cannot directly display complex data types such as Pandas dataframes or Matplotlib figures. Such objects must be serialized before being sent. Matplotlib figures are converted to PNG data URLs, which can be shown using a standard *Image* component. ```python wf.init_state({ "my_matplotlib_fig": fig, }) ``` The element can be used in an *Image* component in the Builder by setting the source to `@{my_matplotlib_fig}`. Alternatively, as data inside a *File Download* component. Plotly graphs are converted to Plotly JS specifications, using JSON. They can be used in *Plotly Graph* components. Altair charts are converted to Vega Lite specifications, based on JSON. They can be used in *Vega Lite Chart* components. Pandas dataframes are converted to JSON and can be used in *Dataframe* components. ## State schema State schema is a feature that allows you to define the structure of the state. This is useful for ensuring that the state is always in the expected format. Schema allows you to use features like * typing checking with mypy / ruff * autocomplete in IDEs * declare dictionaries * automatically calculate mutations on properties more into [Advanced > State schema](./state-schema) ```python import writer as wf class AppSchema(wf.WriterState): counter: int initial_state = wf.init_state({ "counter": 0 }, schema=AppSchema) # Event handler # It receives the session state as an argument and mutates it def increment(state: AppSchema): state.counter += 1 ``` # Authentication The Writer Framework authentication module allows you to restrict access to your application. Framework will be able to authenticate a user through an identity provider such as Google, Microsoft, Facebook, Github, Auth0, etc. Authentication is done before accessing the application. It is not possible to trigger authentication for certain pages exclusively. Static assets from Writer Framework exposed through `/static` and `/extensions` endpoints are not protected behind Authentication. ## Use Basic Auth Basic Auth is a simple authentication method that uses a username and password. Authentication configuration is done in the [server\_setup.py module](/framework/custom-server). Password authentication and Basic Auth are not sufficiently secure for critical applications. If HTTPS encryption fails, a user could potentially intercept passwords in plaintext. Additionally, these methods are vulnerable to brute force attacks that attempt to crack passwords. To enhance security, it is advisable to implement authentication through trusted identity providers such as Google, Microsoft, Facebook, GitHub, or Auth0. ```python server_setup.py import os import writer.serve import writer.auth auth = writer.auth.BasicAuth( login=os.getenv('LOGIN'), password=os.getenv('PASSWORD'), ) writer.serve.register_auth(auth) ``` ### Brute force protection A simple brute force protection is implemented by default. If a user fails to log in, the IP of this user is blocked. Writer framework will ban the IP from either the `X-Forwarded-For` header or the `X-Real-IP` header or the client IP address. When a user fails to log in, they wait 1 second before they can try again. This time can be modified by modifying the value of `delay_after_failure`. ![429](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/429.png) ## Use OIDC provider Authentication configuration is done in the `server_setup.py` [module](custom-server.md). The configuration depends on your identity provider. Here is an example configuration for Google. ![Authentication OIDC Principle](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/auth.png) ```python server_setup.py import os import writer.serve import writer.auth oidc = writer.auth.Oidc( client_id="1xxxxxxxxx-qxxxxxxxxxxxxxxx.apps.googleusercontent.com", client_secret="GOxxxx-xxxxxxxxxxxxxxxxxxxxx", host_url=os.getenv('HOST_URL', "http://localhost:5000"), url_authorize="https://accounts.google.com/o/oauth2/auth", url_oauthtoken="https://oauth2.googleapis.com/token", url_userinfo='https://www.googleapis.com/oauth2/v1/userinfo?alt=json' ) writer.serve.register_auth(oidc) ``` ### Use pre-configured OIDC The Writer Framework provides pre-configured OIDC providers. You can use them directly in your application. | Provider | Function | Description | | -------- | -------------------- | --------------------------------------------------------------------------------------- | | Google | `writer.auth.Google` | Allow your users to login with their Google Account | | Github | `writer.auth.Github` | Allow your users to login with their Github Account | | Auth0 | `writer.auth.Auth0` | Allow your users to login with different providers or with login password through Auth0 | #### Google You have to register your application into [Google Cloud Console](https://console.cloud.google.com/). ```python server_setup.py import os import writer.serve import writer.auth oidc = writer.auth.Google( client_id="1xxxxxxxxx-qxxxxxxxxxxxxxxx.apps.googleusercontent.com", client_secret="GOxxxx-xxxxxxxxxxxxxxxxxxxxx", host_url=os.getenv('HOST_URL', "http://localhost:5000") ) writer.serve.register_auth(oidc) ``` #### Github You have to register your application into [Github](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app#registering-a-github-app) ```python server_setup.py import os import writer.serve import writer.auth oidc = writer.auth.Github( client_id="xxxxxxx", client_secret="xxxxxxxxxxxxx", host_url=os.getenv('HOST_URL', "http://localhost:5000") ) writer.serve.register_auth(oidc) ``` #### Auth0 You have to register your application into [Auth0](https://auth0.com/). ```python server_setup.py import os import writer.serve import writer.auth oidc = writer.auth.Auth0( client_id="xxxxxxx", client_secret="xxxxxxxxxxxxx", domain="xxx-xxxxx.eu.auth0.com", host_url=os.getenv('HOST_URL', "http://localhost:5000") ) writer.serve.register_auth(oidc) ``` ### Authentication workflow ### App static assets Static assets in your application are inaccessible. You can use the `app_static_public` parameter to allow their usage. When `app_static_public` is set to `True`, the static assets in your application are accessible without authentication. ```python oidc = writer.auth.Auth0( client_id="xxxxxxx", client_secret="xxxxxxxxxxxxx", domain="xxx-xxxxx.eu.auth0.com", host_url=os.getenv('HOST_URL', "http://localhost:5000"), app_static_public=True ) ``` ## User information in event handler When the `user_info` route is configured, user information will be accessible in the event handler through the `session` argument. ```python def on_page_load(state, session): email = session['userinfo'].get('email', None) state['email'] = email ``` ## Unauthorize access It is possible to reject a user who, for example, does not have the correct email address. You can also use userinfo inside app. You can restrict access to certain pages inside the application by using the `session` object. See [User information in event handler](#user-information-in-event-handler) ```python from fastapi import Request import writer.serve import writer.auth oidc = ... def callback(request: Request, session_id: str, userinfo: dict): if userinfo['email'] not in ['nom.prenom123@example.com']: raise writer.auth.Unauthorized(more_info="You can contact the administrator at support.example.com") writer.serve.register_auth(oidc, callback=callback) ``` The default authentication error page look like this: | Parameter | Description | | ------------ | ---------------------- | | status\_code | HTTP status code | | message | Error message | | more\_info | Additional information | ## Modify user info User info can be modified in the callback. ```python from fastapi import Request import writer.serve import writer.auth oidc = ... def callback(request: Request, session_id: str, userinfo: dict): userinfo['group'] = [] if userinfo['email'] in ['fabien@example.com']: userinfo['group'].append('admin') userinfo['group'].append('user') else: userinfo['group'].append('user') writer.serve.register_auth(oidc, callback=callback) ``` ## Custom unauthorized page You can customize the access denial page using your own template. ```python import os from fastapi import Request, Response from fastapi.templating import Jinja2Templates import writer.serve import writer.auth oidc = ... def unauthorized(request: Request, exc: writer.auth.Unauthorized) -> Response: templates = Jinja2Templates(directory=os.path.join(os.path.dirname(__file__), "templates")) return templates.TemplateResponse(request=request, name="unauthorized.html", status_code=exc.status_code, context={ "status_code": exc.status_code, "message": exc.message, "more_info": exc.more_info }) writer.serve.register_auth(oidc, unauthorized_action=unauthorized) ``` ## Enable in edit mode Authentication is disabled in edit mode. To activate it, you must trigger the loading of the server\_setup module in edition mode. ```bash writer edit --enable-server-setup ``` # Backend-driven UI Framework facilitates backend-initiated user interface modifications. These changes are made possible through **Code-Managed Components** (CMCs), distinct from the *Builder-Managed Components* (BMCs). CMCs, unlike BMCs, are dynamically created and modified via back-end code, and cannot be edited (but still can be viewed) within the application builder. It's important to also note that CMCs do not persist in your application's files and exist only during the application runtime, supporting dynamic UI adjustments. To summarise: **CMC** – Code-Managed Component * created via **application back-end**; * **cannot be edited** in builder; * is **not saved** to `.wf/components-*.jsonl`. **BMC** – Builder-Managed Component * created via **builder**; * **can be edited** in builder; * is **saved** to `.wf/components-*.jsonl`. ## UI manager Framework provides two independent approaches for managing your application's UI: initializing a base UI and making session-specific updates. ### Initializing base UI The `init_ui()` method sets up a UI manager to configure UI components at the application's startup. This creates a component set that is accessible across all sessions: ```python import writer as wf with wf.init_ui() as ui: with ui.Page(id="my-page"): ui.Header({"text": "Hello World!"}) ui.ColumnContainer(id="column-container") ``` ### Making session-specific updates For dynamic, session-specific UI updates, the `ui` parameter is used within handler functions. This approach allows for real-time modifications tailored to individual user sessions: ```python def display_user_data(ui, state): with ui.find("column-container"): with ui.Column(): ui.Text({"text": f"And welcome {state["username"]}!"}) with ui.Column(): ui.Text({"text": f"Your data: {state["user_data"]}"}) ``` ## UI manager methods ### `find` method You can use the `ui.find(component_id: str)` method to access existing components by ID: ```python with ui.find("column-container"): with ui.Column(): ... ``` If the component couldn't be found, the method raises a `RuntimeError`. ### `refresh_with` method You can use the `ui.refresh_with(component_id: str)` method to replace children CMCs of an existing component (referenced by its ID): ```python with ui.refresh_with("my-page"): # Previously existing children are cleared ui.Header({"text": "Hello New World!"}) with ui.ColumnContainer(): with ui.Column(): ui.Text({"text": "Nobody here for now..."}) ``` This method also allows to clear children CMCs of a component: ```python with ui.refresh_with("my-page"): # Empties the page pass ``` If a targeted component has builder-managed children, they will not be removed. A warning message will be recorded in the application's log for each BMC attempted to be removed. This does not stop the execution of the method – any remaining CMCs will still be removed. As well as with `find` method, it also raises a `RuntimeError` if it fails to find a referenced component. ### `parent` method `ui.parent(component_id: str, level: int = 1)` gives access to the id to parents at higher levels. ```python container = ui.parent('my-text') # first parent id container = ui.parent('my-text', 3) # level 3 parent id with ui.find(container): ... ``` ### Component methods UI manager contains methods linked to each front-end component. For example, in previous code snippets we provide a `ui.Text` method, which is used for creating [Text components](https://dev.writer.com/components/text). This method expects `content: dict` as first argument, which enables you to set the field properties of the component, through corresponding keys: ```python ui.Text( { "text": "Hello World!", # The text content of the component "useMarkdown": "no", # Will not use Markdown "alignment": "left", # Text is aligned to the left "primaryTextColor": "#000000", # The text color is black "cssClasses": "my-text hello-world" # Apply 'my-text' and 'hello-world' CSS classes } ) ``` In a similar way, every other component method also expects `content` as its first argument: ```python ui.VideoPlayer( { "src": "https://example.com/assets/mov/rick-roll-video.mov", "autoplay": "yes", "controls": "no", "muted": "no", "loop": "no", } ) ``` In addition to `content`, a set of fields which is specific to the component type, you can also modify the base properties of the component itself, which are: * **`id: str`**: A unique identifier used for accessing the component after it was created.\ *Providing an identifier that is already taken would result in `RuntimeWarning` and the existing component being overwritten with a newly created one.* ```python ui.Text( {"text": "Hello World!"}, id="hello-world-text" ) ``` *If no ID is provided with a component, a UUID is automatically generated for it.* Make sure to provide an `id` if you intend to `find` the component later\ As the `find` method relies on `id` of the component, retrieval might get tricky if its `id` was generated randomly. * **`position: int`**: Determines the display order of the component in relation to its siblings.\ Position `0` means that the component is the first child of its parent.\ Position `-2` is used for components – such as [sidebars](https://dev.writer.com/components/sidebar) – that have a specific reserved position not related to their siblings. ```python ui.Text( {"text": "Hello Parent, I'm your first child!"}, position=0 ) ``` *Position is calculated automatically for each component, and you should be careful when you override it with predefined value, as this might lead to unexpected results.* * **`parentId: str`**: Determines the parent [container](#container-components) for the component. By default, components recognise the container in the context of which they were defined as their parent. This allows for linking components to their parents outside of context, or for overriding a parent within a context. ```python ui.Text( {"text": "Hello Parent, I'm your child too!"}, parentId="dear-parent" ) ``` * **`visible: bool | str`**: Determines the visibility of the component, `True` by default. ```python ui.Text({"text": "I'm visible!"}, visible=True) ui.Text({"text": "And I'm not!"}, visible=False) ui.Text({"text": "My visibility depends on the @{my_var}!"}, visible="my_var") ``` * **`handlers: dict[str, callable]`**: Attaches [event handlers](https://dev.writer.com/framework/event-handlers) to the component. Each dictionary key represents an event, and its value is the corresponding handler.: ```python def increment(state): state["counter"] += 1 initial_state = wf.init_state({"counter": 0}) ... ui.Button( {"text": "My Counter: @{counter}"}, handlers={"wf-click": increment} ) # You have two options for adding a function # to the `handlers` dictionary: # directly pass the function itself, # or use the function's name as a string. # Both approaches yield the same outcome. ``` *A component can be linked to multiple event handlers.* * **`binding: dict[str, str]`**: Links the component to a state variable via [binding](https://dev.writer.com/framework/builder-basics#binding). The dictionary key is the bindable event, and the value is the state variable's name: ```python initial_state = wf.init_state({ "header_text": "Default Text" "counter": 0 }) ... ui.TextInput( {"label": "Bound Text"}, binding={"wf-change": "header_text"} ) # This input will display "Default Text" # Changing the text in this input will modify the `header_text` variable ui.SliderInput( {"minValue": 0, "maxValue": 300, "stepSize": 1}, binding={"wf-number-change": "counter"} ) # This slider will have 0 as a default value # Sliding it will modify the `counter` variable ``` *Unlike handlers, a component can be linked to just one variable via a bindable event. If the `binding` dictionary includes multiple event-variable pairs, a `RuntimeError` will be triggered.* ### Container components Framework provides multiple layout components that can serve as *containers* for other components. You can use `with` keyword to define such layouts: ```python with ui.Section({"title": "My Section"}): ui.Text({"text": 'Hello World!'}, id="hello-world") ``` It also allows for "chaining" multiple containers together, creating extensive and deeply-nested layout structures when needed: ```python with ui.ColumnContainer(id="cmc-column-container"): with ui.Column(id="cmc-column-1"): with ui.Section({"title": "My Section 1"}): ui.Text({"text": 'Hello World!'}, id="hello-world-1") with ui.Column(id="cmc-column-2"): with ui.Section({"title": "My Section 2"}): ui.Text({"text": 'Hello World again!'}, id="hello-world-2") ``` Most components depend on being inside of a container. This means, for example, that Text components in code above cannot be created as "orphans", outside a Column or Section. Attempting to do so would raise an `UIError`. By default, components inside container's `with` are being *appended* to it: ```python with ui.Column(id="cmc-column-1"): ui.Text({"text": 'Hello World!'}, id="hello-world-1") ... # Retrieves the Column component created before with ui.find(id="cmc-column-1"): # The following component is going to be appended # to the retrieved Column ui.Text({"text": 'Hello World again!'}, id="hello-world-2") ``` This will result in a Column component having two children Text components. To replace or clear the children, use [`refresh_with` method](#refresh_with-method): ```python with ui.Column(id="cmc-column-1"): ui.Text({"text": 'Hello World!'}, id="hello-world-1") ... with ui.refresh_with(id="cmc-column-1"): # The following component is going to replace # previously existing children of the retrieved Column ui.Text( {"text": 'To Hello World, or not to Hello World?'}, id="hello-world-new" ) ``` # Backend-initiated actions Targeted, backend-initiated actions can be triggered from event handlers, using methods of `state`. Internally, this is achieved using Framework's `mail`, ephemeral state that is cleared when it reaches the intended user. ## Triggering a file download The `file_download` method takes the `data` and `file_name` arguments. The first must contain raw bytes (a `bytes` object) or a packed file. As mentioned in the [Application State](application-state.html#files-and-binary-data) section of the guide, a packed file is obtained using the `wf.pack_file` or `wf.pack_bytes` methods. ```py def handle_file_download(state): # Pack the file as a FileWrapper object data = wf.pack_file("assets/story.txt", "text/plain") file_name = "thestory.txt" state.file_download(data, file_name) ``` ## Adding a notification ![Notifications](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/backend-initiated-actions.notifications.png) Framework adds notifications when a runtime error takes place. You can add your own notifications using the `add_notification` method, which takes the `type`, `title` and `message` arguments. `type` must be one of `error`, `warning`, `info`, `success`. ```py def notify_of_things_that_happened(state): state.add_notification("error", "An Error", "Something bad happened.") state.add_notification("warning", "A Warning", "Be aware that something happened.") state.add_notification("info", "Some Info", "Something happened.") state.add_notification("success", "A Success", "Something good happened.") ``` ## Opening a URL Open a URL in a new tab using the `open_url` method, which takes the `url` argument. ```py def handle_open_website(state): state.open_url("https://writer.com") ``` The URL will be safely opened with `noopener` and `noreferrer` options. Popup blockers: Given that the URL is opened asynchronously, popup blockers will likely block the new window —unless the user has opted in. ## Changing the active page The active page and route parameters can be changed using the methods `set_page` and `set_route_vars`. This is explained in more detail in [Page Routes](page-routes.html). # Builder basics Framework Builder works as an overlay of the running app; you edit your app while it's running. It gives you an accurate representation of what the app will look like and how it'll behave, without the need to constantly preview it. Changes to the user interface are automatically saved into `.wf/` folders. ## Modes You can switch modes between *User Interface*, *Code* and *Preview* using the buttons on the top bar. ### User Interface ![Framework Builder - Mode: User Interface](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/builder-basics.ui.png) The default mode. Allows you to focus on building the interface. ### Code ![Framework Builder - Mode: Code](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/builder-basics.code.png) This mode displays the **code editor** and the **application log**, while still allowing you to access the *Component Tree* and *Settings*. Code changes are automatically detected. The application will reload whenever a change to a `.py` file inside the app folder is detected. This feature only works in Framework Builder i.e. `edit` mode, not when running the app in `run` mode. The built-in code editor for `main.py`, the entry point of your application. This editor is provided for convenience and is ideal for quick edits — but you don't need to rely on it. If you need a more powerful editor or if your codebase is distributed across several files, use a local editor. Exceptions raised by your application are shown here, as log entries. Standard output from your application is also captured and displayed as log entries. ### Preview ![Framework Builder - Mode: Preview](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/builder-basics.preview.png) The *Preview* mode shows the application exactly like the user will see it. It allocates the whole width of the viewport to the app. ## Adding and moving components You can create new components in your app by dragging and dropping items from the Toolkit. Some components, like Sections, can act as parents, while others, such as Text, cannot. Additionally, certain components have placement restrictions—for instance, a Column must be added to a Column Container, and a Sidebar can only be added to a Page. By default, components are positioned at the end, but if you need to place them specifically, simply drag them over the desired parent until you see the insertion lines. You can also reorganize existing components by moving them between parents or within the same parent. For more flexibility, the Component Tree can serve both as a source or a destination for your drag and drop actions. ## Selecting a component Select a component by clicking on it. If you click on a component that's already selected, the click will be treated as an interaction with the app. Two things will happen when a component is selected: The *Component Settings* panel will open on the right. Depending on available screen real estate, the panel may open on top of the app or next to it. A set of component-specific actions, *Component Shortcuts*, will be displayed on top of the component. ## Component settings Settings are divided into the following sections. Changes to settings can be undone and redone using the buttons on the top bar. ![Framework Builder - Component settings](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/builder-basics.component-settings.png) Divided into *General* and *Style* categories. Values can include: 1. Literals, e.g. `monkey` 2. References to application state using the template syntax `@{}`, e.g. `@{my_favourite_animal}`. 3. A combination of both, e.g. `My favourite animal is @{my_favourite_animal}`. 4. Nested states can be accessed with `.` (dot), e.g. `@{building.height}`. 5. Nested elements can be dynamically accessed with `[]`, e.g. `@{building[dynamic_prop]}` will be equivalent to `@{building.height}` when `dynamic_prop` equals `height`. Properties are of different types, such as *Text*, *Color* and *Number*. All property values are stored as text values, then casted when being evaluated. ![Framework Builder - Binding](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/builder-basics.binding.png) Input components can be bound, in a two-way fashion, to a state element. For example, a *Slider Input* component can be bound to `my_var`. If the value of the slider changes, so does the value of `my_var`. Similarly, if the value of `my_var` changes, the slider is moved automatically to reflect the change. To bind an input component, specify the state element. For example, `my_var` or `building.height`. Note that this field should not contain the template syntax, e.g. `my_var` and not `@{my_var}`. The events generated by this component, with the option of setting event handlers for those. Event handlers are explained in more detail in a separate section of this guide. Whether the component should be displayed. There are three visibility options: 1. Yes. The component is displayed. 2. No. The component isn't displayed. Note that hidden components are still part of the HTML code but aren't shown. 3. Custom. Whether the component is displayed or not depends on the value of a state or context element. For example, if set to `my_var`, visibility will depend on the value of the `my_var` state element. Note that this field, similarly to Binding, should only contain the state element, e.g. `my_var` and not `@{my_var}`. ## Component shortcuts Perform a variety of operations on existing components. Options will be grayed out when they're not applicable to the relevant component. Most shortcuts can be triggered using the keyboard; hover on them to show the appropriate combination. ![Framework Builder - Component shortcuts](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/builder-basics.component-shortcuts.png) Adds a child of a specified type to this component. Decrements the position index of the component, used to sort children within the parent container. Increments the position index of the component. Cuts the component and places it into Builder’s internal clipboard. Copies the component and places it into the internal clipboard. Pastes the content of the internal clipboard using the selected component as a parent. Selects the parent of the selected component. Deletes this component. Just like with changes to settings, these operations can be undone and redone. ## Discovering components The Builder is designed to allow easy discoverability of components. Rather than scouring specifications every time you need to use a component, you can rely on the visual editor to guide you. 1. **Short description:** You can hover on the component type to get a tooltip with a short description. 2. **Available properties and events:** Looking at *Settings* will allow you to see which of its properties are configurable. 3. **Built-in docs:** Components have short docs built into them. You can expand it by clicking the help icon in Settings. 4. **Event handler stub code:** Different events need to be handled differently. The built-in stub handlers, which can be found next to each event, can help you get started when writing event handlers. # Chat assistant In this tutorial, you'll use the Writer Framework to create a simple yet powerful chat assistant that can engage in conversations on various topics, provide answers to your questions, and maybe even help you when you're experiencing writer's block! The process will take only minutes using a drag-and-drop visual editor to build the user interface and Python for the back-end code. Here's what the finished project will look like: ![Finished chat assistant project](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/chat/chat_assistant_1.png) ## Prerequisites Before starting, ensure you have: * **A Writer account:** You don't need an account to use Writer Framework, but you'll need one to use the AI module. [Fortunately, you can sign up for an account for free!](https://app.writer.com/aistudio/signup) * **Python 3.9.2 or later**: Use the installer from [python.org](https://www.python.org/downloads/). * **pip:** This command-line application comes with Python and is used for installing Python packages, including those from Writer. * **A basic understanding of Python:** You should be familiar with the basics of the language. * **Your favorite code editor (optional):** There's a code editor built into Writer for editing back-end code, but you can also use Visual Studio Code, Notepad++, Vim, Emacs, or any text editor made for programming if you prefer. ## Setting up your project ### Create a Writer app and get its API key First, you'll need to create a new app within Writer. Log into Writer. From the Home screen, click on the **Build an app** button. ![Writer home screen](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/chat/chat_assistant_2.png) The **Start building** menu will appear, presenting options for the types of apps you can create. Select **Framework**, located under **Developer tools**. This will create a brand new app based on Writer Framework. !["Start building" menu](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/chat/chat_assistant_3.png) On the next screen, titled **How to deploy an application**, you can get the API key for the app by clicking on the **Reveal key** button, located under the text **Authenticate with an API key**. Your complete API key will be displayed, and a "copy" button will appear. Click this button to copy the key; you'll use it in the next step. !["How to deploy an application" page](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/chat/chat_assistant_2a.png) ### Set up your computer and create the app's project The next step is to set up the Writer Framework environment on your computer. You'll do this by creating a directory for the project, installing dependencies, and creating the project for the application using a template. Open your terminal application. On macOS and Linux, this application goes by the name *Terminal*; on Windows, you can use either *Windows PowerShell* (which is preferred) or *Command Prompt*. If you already have the `writer` and `python-dotenv` packages installed on your computer, you can skip this step. Install the `writer` and `python-dotenv` packages by entering the following commands in your terminal application: ``` pip install writer python-dotenv ``` This command tells `pip`, the Python package installer, to install two packages: * `writer`, which provides some command-line commands and enables Python code to interact with Writer and the Writer Framework. * `python-dotenv`, which makes it easy to manage environment variables by loading them from a `.env` file. This one is optional for this exercise, but you might find it useful when working on larger projects. To pass your API key to the Writer Framework, you need to set an environment variable called `WRITER_API_KEY`. Select your operating system and terminal application below, then copy and paste the command into your terminal application, replacing `[your_api_key]` with the API key you copied earlier: ```sh macOS/Linux (Terminal) export WRITER_API_KEY=[your_api_key] ``` ```sh On Windows (Windows PowerShell) $env:WRITER_API_KEY=[your_api_key] ``` ```sh On Windows (Command Prompt) set WRITER_API_KEY=[your_api_key] ``` The `WRITER_API_KEY` environment variable will remain defined as long your terminal session is open (that is, until you close your terminal application’s window). Create the project by entering this command into your terminal application: ``` writer create chat-assistant --template=ai-starter ``` This command sets up a new project called `chat-assistant` using a starter template called `ai-starter` so that you're not starting "from scratch." ## Build the UI Now that you've created the project, it's time to define the UI. The Writer Framework's drag-and-drop capabilities make it easy — even if you haven't done much UI work before! The project editor is a web application that runs on your computer and enables you to define and edit your app's user interface. Launch it by typing the following into your terminal application: ``` writer edit chat-assistant ``` You'll see a URL. Control-click it (command-click on macOS) to open it, or copy the URL and paste it into the address bar of a browser window. The browser window will contain the project editor, which will look like this: ![Project editor](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/chat/chat_assistant_2b.png) You'll see the following: * The **canvas** is in the center. It displays the app's user interface. * The column on the left contains: * The **Core toolkit**, which contains all the UI components. You define the user interface by dragging components from the Toolkit and placing them on the canvas. * The **Component tree**, which shows the arrangement of the UI components on the canvas. It's also useful for selecting items on the canvas, especially when it has a lot of UI components. It's time to build the UI! Select the **Header** component by clicking it — it's the component at the top, containing the title **AI STARTER** and a gray area labeled **Empty Header**. When you click it, you'll see the **properties** panel appear on the right side of the page. This lets you view and edit the properties of the selected component. ![The selected header and its properties panel](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/chat/chat_assistant_2c.png) The first property you'll see in the panel is the **Text** property, which defines the text that appears as the header's title. It should contain the value `@{my_app.title}`. The `@{` and `}` indicate that `my_app.title` is a variable and that its contents should be the text displayed instead of the literal text "my\_app.title". You'll set the value of this variable soon. Select the **Section** component by clicking it — it's just below the **Header** component and contains the title **Section Title** and a gray area labeled **Empty Section**. In the **properties** panel, clear out the value of the **Title** property. This will remove the **Section**'s default title. ![The selected section and its properties panel](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/chat/chat_assistant_2d.png) Drag a **Text** component from the **Core toolkit** panel on the left (it's under **Content**, and you may need to scroll down a little to find it) and into the *Section*. Sections can act as containers for other components. You can search for a specific component by using the search bar at the top of the **Core toolkit** panel. Select the **Text** component. In the **properties** panel, set the **Text** property to provide instructions or context for your chat assistant. Here's an example: `Welcome to the Chat Assistant. Ask me anything!` ![The text component and its properties panel](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/chat/chat_assistant_2e.png) The heart of this app is the **Chatbot** component, a pre-built component that displays the conversation between the LLM and the user and provides a text field where the user can enter prompts. Drag a **Chatbot** component from the **Core toolkit** panel (it's under **Content**) into the *Section*, just below the Text box. ![The chatbot component](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/chat/chat_assistant_2f.png) ## Add the back-end code With the UI laid out, it's time to work on the logic behind it. The logic behind the user interface is defined in a file named `main.py`, which is in your project's directory. This file was automatically generated; you'll update the code in it to define the behavior of your app. The simplest way to edit `main.py` is within the project editor. Click on the "toggle code" button (beside the word **Code**) near the lower left corner of the project editor page. ![Project editor with arrow pointing to toggle code button](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/chat/chat_assistant_2g.png) A pane with the name **Code** will appear at the bottom half of the screen, displaying an editor for the the contents of `main.py`. ![Project editor with the code editor displayed](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/chat/chat_assistant_2h.png) If you'd rather use a code editor instead of coding in the browser, use it to open the `main.py` file in your project's directory. Now follow these steps: You should see the following at the start of the file: ```python import writer as wf import writer.ai ``` Replace that code with the following: ```python import os import writer as wf import writer.ai # Set the API key wf.api_key = os.getenv("WRITER_API_KEY") ``` This code imports the libraries that the application will need and then reads your Writer Framework API key in the `WRITER_API_KEY` environment variable. The application needs a function to handle incoming chat messages. Find these comments in the code... ```python # Welcome to Writer Framework! # This template is a starting point for your AI apps. # More documentation is available at https://dev.writer.com/framework ``` ...and replace them with the following function: ```python def generate_completion(state, payload): print(f"Here's what the user entered: {payload['content']}") state["conversation"] += payload print(f"Conversation: {state['conversation'].messages}") try: for index, chunk in enumerate(state["conversation"].stream_complete()): print(f"Chunk {index}: {chunk}") if not chunk.get("content"): chunk["content"] = "" state["conversation"] += chunk print(f"state['conversation']:\n{state['conversation'].messages}") except Exception as e: print(f"Error during stream_complete: {e}") ``` The `generate_completion()` function will be called when the user enters a prompt, which is contained in the `payload` object. The `payload` object is added to the `conversation` object contained in the application's `state`, which adds the user's prompt to the record of the conversation between the user and the LLM. After adding the user's prompt to the conversational record, `generate_completion()` calls the `conversation` object's `stream_complete()` method, which generates an LLM completion based on the conversation so far. As its name implies, `stream_complete()` returns the completion as a stream of text chunks, which are captured and added to the `conversation` object. The `conversation` object in the code above is an instance of Writer’s `Conversation` class. You can find out more about this class on our [*Writer AI module*](https://dev.writer.com/framework/ai-module) page. Note that `generate_completion()` completion uses a lot of `print()` functions for debugging purposes, and you can use them to get a better idea of what's happening in the function. You'll see their output in both your terminal application and in the project editor's 'log' pane (which will be covered shortly) as you use the chat assistant. This output will include: * The prompt the user entered * The chunks of data that make up the LLM's response as they are generated * The record of the conversation between the user and the LLM. The `print()` functions don't affect the operation of the chat assistant in any way, and you can remove them if you wish. The final step is to set the application's initial state. Find this code, which should be just after the `generate_completion()` function... ```python # Initialise the state wf.init_state({ "my_app": { "title": "AI STARTER" }, }) ``` ...and replace it with this: ```python # Initialize the state wf.init_state({ "conversation": writer.ai.Conversation(), "my_app": { "title": "CHAT ASSISTANT" }, }) ``` The Writer Framework's `init_state()` method sets the initial value of `state`, a dictionary containing values that define the state of the application. The key-value pairs in `state` are how you store values used by your app and how you pass data between the back-end code and the UI. The code above sets the initial value of `state` so that it has two key-value pairs: * `conversation`: An object that keeps a record of the conversation that the user is having with the LLM. You'll bind its value to the **Chatbot** component soon. * `my_app`: A dictionary containing values that define the application's appearance. This dictionary has a single key-value pair, `title`, which defines the text that appears as the application's title in the **Header**. For more details about the `state` variable, see our [*Application state*](https://dev.writer.com/framework/application-state#application-state) page. That’s all the code. If you edited the code in the browser, save it by clicking the “save” button near the top right corner of the code editor. ![Project editor and code editor, with arrow pointing to save button](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/chat/chat_assistant_2i.png) Click the "toggle code" button to hide the code editor. ## Bind the UI to the back-end code You've built the UI and written the code behind it. Let's connect the two! Go back to the browser window with the project editor and do the following: Earlier, you saw that the **Header** component's **Text** property was set to `@{my_app.title}`, a value in the app's `state` variable. You changed this value when you update the call to the Writer Framework's `init_state()` method. Recall that the `conversation` object contained within the `state` variable contains the record of the conversation that the user is having with the LLM. Binding the **Chatbot** component to this object allows it to display the conversation to the user. Select the **Chatbot** component. In the **properties** panel, find the **Conversation** property and set its value to `@{conversation}`. ![Updating the Chatbot's conversation property](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/chat/chat_assistant_2j.png) The value `@{conversation}` specifies that the **Chatbot** component should get its information from the value corresponding to the `conversation` key in the application's `state` variable. You need to specify that the **Chatbot** component should call the `generate_completion()` function when the user enters a prompt. Do this by scrolling down the **properties** panel to the **Events** section until you see a property called **`wf_chatbot_message`**. Select **`generate_completion`** from its menu. ![Updating the Chatbot's wf\_chatbot\_message property](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/chat/chat_assistant_2k.png) ## Test the application You've completed all the steps to make a working chat assistant, and you can try using it right now, even while editing the user interface! Try entering some prompts into the text entry at the bottom of the **Chatbot** component. The LLM should respond accordingly: ![The chat assistant, with the project editor in "UI" mode](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/chat/chat_assistant_2l.png) To get a better sense of what the experience will be like for the user, switch to the preview by changing the edit mode (located near the upper left corner of the page) from *UI* mode to *Preview* mode by selecting the **Preview** option: ![The project editor with an arrow pointing to the Preview button](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/chat/chat_assistant_2m.png) Here’s what the app looks like in *Preview* mode: ![The chat assistant, with the project editor in "Preview" mode](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/chat/chat_assistant_2n.png) You can see the output of any `print()` functions and error messages by clicking on the **Log** button located near the upper right corner of the page: ![The chat assistant with an arrow pointing to the Log button](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/chat/chat_assistant_2o.png) Here’s what the app looks like when displaying the log: ![The working chat assistant, with the log pane displayed](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/chat/chat_assistant_2p.png) It's very helpful to be able to test the application while editing it. As you continue to work with Writer Framework, you'll find yourself alternating between making changes to your application and testing those changes without having to leave the project editor. ## Run the application locally Once you've tested the application, it's time to run it locally. Switch back to your terminal application. Stop the project editor with ctrl-c, then run the application by entering the following command: ``` writer run chat-assistant ``` Note that the command starts with `writer run` as opposed to `writer edit`. This launches the application as your users will see it, without any of the editing tools. Even though you can preview your applications in the project editor, it's still a good idea to test it by running it on your computer, outside the project editor, before deploying it. You'll be able to access the application with your browser at the URL that appears on the command line. It should look like this: ![Finished chat assistant project](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/chat/chat_assistant_1.png) The Writer editor, which you launched with `writer edit chat-assistant`, and your application, which you launched with `writer run chat-assistant`, run on the same URL, but on different *ports* (specified by the number after the `:` character at the end of the URL). ## Deploy the app to the Writer Cloud (optional) Right now, the app will only run on your computer. To make it available online, you'll need to deploy it to the Writer Cloud. In your terminal application, stop your app with ctrl-c, then deploy your application by entering the following command: ``` writer deploy chat-assistant ``` You'll be asked to enter your app's API key. Once you do that, the Writer command-line application will start deploying your application to the Writer Cloud. The process should take a couple of minutes. Once the app has been deployed to the Writer Cloud, you'll be shown the URL for your application, which you can use to access it online. ## Conclusion That's it — you've built a functional chat assistant using the Writer Framework! Feel free to modify this project! The Writer platform is flexible enough for you to customize, extend, and evolve your application into something completely different! To find out what else you can do, check out the documentation for [Writer Framework](https://dev.writer.com/framework/introduction) and the [Writer API](https://dev.writer.com/api-guides/introduction). # Deploy to Writer Cloud ## Basic usage The `writer cloud` command group includes the following commands: * `deploy` * `undeploy` * `logs` ## Common options These options are common across multiple commands: * `--api-key`: The Writer API key used for authentication. If not provided, you will be prompted to enter it. * `--verbose, -v`: Enable verbose output. ## Commands Deploys an app from the specified path to the Writer cloud. **Usage:** ``` writer cloud deploy [OPTIONS] PATH ``` **Arguments:** * `PATH`: The path to the folder containing the app to deploy. **Options:** * `--api-key`: Writer API key for authentication. If not provided, you will be prompted to enter it. * `--env, -e`: Environment variables to set in the deployed app. Use the format `VAR=value`. Multiple environment variables can be specified by repeating the `--env` option. * `--verbose, -v`: Enable verbose output. **Example:** ``` writer cloud deploy hello --env VAR1=value1 --env VAR2=value2 ``` **Description:** * Deploys the app located in the `PATH` folder. * Creates a deployment package, ignoring `.git` directories, `Dockerfile`s and all files specified in `.gitignore` file. * Uploads the package to the deployment server. * The deployed app will have access to specified environment variables. * By default, the `WRITER_API_KEY` environment variable will be added to enable AI features. **Output Example** ``` Creating deployment package from path: /path/to/your/app [WARNING] Dockerfile found in project root. This will be ignored in the deployment package. Packing file: pyproject.toml Packing file: README.md ... Uploading package to deployment server Package uploaded. Building... ... Deployment successful URL: https://your_app_url ``` Stops the deployed app and makes it unavailable. **Usage:** ``` writer cloud undeploy [OPTIONS] ``` **Options:** * `--api-key`: Writer API key for authentication. If not provided, you will be prompted to enter it. * `--verbose, -v`: Enable verbose output. **Example:** ``` writer cloud undeploy ``` **Description:** * Stops and removes the deployed app from the Writer cloud. Fetches logs from the deployed app. **Usage:** ``` writer cloud logs [OPTIONS] ``` **Options:** * `--api-key`: Writer API key for authentication. If not provided, you will be prompted to enter it. * `--verbose, -v`: Enable verbose output. **Example:** ``` writer cloud logs ``` **Description:** * Continuously fetches and displays logs from the deployed app. * Logs are ordered by date and time. **Output Example** ``` 2024-06-11 09:27:02.190646+00:00 [INFO] Starting container entrypoint... 2024-06-11 09:27:03.798148+00:00 [INFO] BuildService - Downloading build files... ... ``` ## Environment variables When deploying an app, you can specify environment variables that will be available to the app during runtime. Use the `--env` option to pass these variables. ``` writer cloud deploy hello --env DB_HOST=db.example.com --env DB_PORT=5432 ``` In this example, `DB_HOST` and `DB_PORT` will be available to the app as environment variables. ## API key The `WRITER_API_KEY` is crucial for deploying and managing apps. It is used for authenticating requests to the Writer cloud. If not provided as an option, the CLI will prompt you to enter it. The `WRITER_API_KEY` will also be added to the deployed app's environment to enable AI features of the Writer framework. ## Deployment process The CLI packages the app, excluding certain files (e.g., Dockerfile, `service_entrypoint.py`). The package is uploaded to the Writer deployment server. The server builds and deploys the app, making it accessible via a URL. Specified environment variables are set, and `WRITER_API_KEY` is added by default. ## Example workflow ```bash writer cloud deploy /path/to/app --env DB_HOST=db.example.com --env DB_PORT=5432 ``` ```bash writer cloud logs ``` ```bash writer cloud undeploy ``` By following this documentation, you should be able to effectively deploy and manage your Writer apps using the Writer Framework CLI. # Components # Custom components It's possible to extend Framework with custom component templates. They're developed using Vue 3 and TypeScript. Once transpiled, they can be used by copying them to the `extensions/` folder of any project. Custom components behave exactly like built-in ones. They are just as performant, can contain other components, and offer the same the Builder experience. They only differ from built-in components in the way that they're bundled and imported. ## Architecture Framework front-end compiles to a collection of static assets that is distributed in the Python package. These static assets are then served via FastAPI. During initialisation time, the server scans the `extensions/` folder in the project folder and looks for `.css` and `.js` files. This folder is also served, similarly to `static/`. If it finds any valid files in `extensions/`, it shares the list with clients and tells them to dynamically import these files during runtime. Extensions and custom templates are currently synonyms, but this might change in order to accommodate other extension capabilities. ![Custom Components - Architecture](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/custom-components.architecture.png) Dependencies are [provided](https://vuejs.org/api/composition-api-dependency-injection.html) using injection symbols and can be *injected* to be used by the component template. These include `evaluatedFields`, which contain the current values of the editable fields. Injected dependencies are fully typed, making development easier. [Rollup's external feature](https://rollupjs.org/configuration-options/#external), invoked via Vite, allows for extensions to be compiled without dependencies and link those during runtime. Therefore, extensions aren't bundled to be standalone, but rather to work as a piece of a puzzle. ![Custom Components - External](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/custom-components.external.png) ## Anatomy of a template A template defines how a certain component is rendered. For example, `corebutton` defines how *Button* components are rendered. Framework component templates are purely front-end. They are Vue 3 templates that extend the Vue specification via a [custom option](https://vuejs.org/api/utility-types.html#componentcustomoptions), `writer`. This custom option defines all the Framework-specific behaviour of the component. For example, its `fields` property establishes which fields will be editable via the Builder. ### Simple example This example shows a template for *Bubble Message*, a simple demo component with one editable field, `text`. ```js ``` The code above will make Bubble Message available in the Builder. ![Custom Components - Bubble Message](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/custom-components.bubble-message.png) ## Developing templates ### Run a local server To get started, clone the [Framework repository](https://github.com/writer/writer-framework) from GitHub. To develop custom templates in a developer-friendly way, ensure you have a front-end development server with instant reload capabilities. The front-end code for Framework is located in the `ui` folder. With Node and npm installed on your system, run `npm install` to install dependencies. Then, start the server with support for custom component templates using `npm run custom.dev`. ```sh cd ui npm install # "custom.dev" links templates in "custom_components/" # "dev" runs the server without them npm run custom.dev ``` The command `npm run custom.dev` starts a front-end server, which requires a back-end to function fully. Start Framework via command line, specifying the option `--port 5000`, to provide a back-end on that port. It's recommended to create a new app for testing the template you're developing. `sh writer create customtester writer edit customtester --port 5000 ` You should now be able to access Framework via the URL provided by Vite, e.g. `http://localhost:5174`. In the Builder's *Toolkit*, you should see the sample component, *Balloon Message*. Add it to your tester application. ### Create a new component You can also have a look at the built-in component templates, since their syntax is equivalent. They can be found in the `ui/src/components/core` folder. Go to `ui/src/components/custom` and open the Vue single-file components, i.e. the `.vue` files. These files contain comments that will help you get started. Try editing the provided templates, you should see changes reflected. You can get started by duplicating one of these examples. Make sure you add the new template to the entrypoint, as discussed below. ### Define entrypoint For custom component templates to be taken into account, they need to be accessible from the entrypoint. Edit `ui/src/components/custom/index.ts` to define which templates you wish to export and under which identifiers. ```ts // Import the templates import BubbleMessage from "./BubbleMessage.vue"; import BubbleMessageAdvanced from "./BubbleMessageAdvanced.vue"; // Export an object with the ids and the templates as default export default { bubblemessage: BubbleMessage, bubblemessageadvanced: BubbleMessageAdvanced, }; ``` A single or multiple templates can be specified. Take into account that they will all be exported, and later imported, together. ## Bundling templates Execute `npm run custom.build` into `src/ui`, this will generate the output `.js` and `.css` files into `./custom_components_dist`. ```sh # "build" builds the entire front-end # "custom.build" only builds the custom templates npm run custom.check # Optional: checks certain issues on custom components npm run custom.build ``` Collect the files from `./custom_components_dist` and pack them in a folder such as `my_custom_bubbles`. The folder containing the generated files, e.g. `my_custom_bubbles`, can now be placed in the `extensions/` folder of any Framework project. It'll be automatically detected during server startup. The `custom.check` command is optional, but it's recommended to run it before building the custom components. It checks for common issues in the custom components, such as invalid key declaration, ... # Custom server Framework uses Uvicorn and serves the app in the root path i.e. `/`. If you need to use another ASGI-compatible server or fine-tune Uvicorn, you can easily do so. ## Configure webserver You can tune your server by adding a `server_setup.py` file to the root of your application, next to the `main.py` files. This file is executed before starting writer. It allows you to configure [authentication](./authentication.md), add your own routes and middlewares on FastAPI. ```python # server_setup.py import typing import writer.serve if typing.TYPE_CHECKING: from fastapi import FastAPI # Returns the FastAPI application associated with the writer server. asgi_app: FastAPI = writer.serve.app @asgi_app.get("/probes/healthcheck") def hello(): return "1" ``` `server_setup.py` is disabled by default on edit mode. If you want to use in `edit` mode, you can launch `writer edit --enable-server-setup `. ## Implement custom server You can import `writer.serve` and use the function `get_asgi_app`. This returns an ASGI app created by FastAPI, which you can choose how to serve. The following code can serve as a starting point. You can save this code as `serve.py` and run it with `python serve.py`. ```py import uvicorn import writer.serve app_path = "." # . for current working directory mode = "run" # run or edit asgi_app = writer.serve.get_asgi_app(app_path, mode) uvicorn.run(asgi_app, host="0.0.0.0", port=5328, log_level="warning", ws_max_size=writer.serve.MAX_WEBSOCKET_MESSAGE_SIZE) ``` Note the inclusion of the imported `ws_max_size` setting. This is important for normal functioning of the framework when dealing with bigger files. Fine-tuning Uvicorn allows you to set up SSL, configure proxy headers, etc, which can prove vital in complex deployments. If you want to disable server setup hook, you should use `enable_server_setup`: ```python asgi_app = writer.serve.get_asgi_app(app_path, mode, enable_server_setup=False) ``` ## Multiple apps at once Framework is built using relative paths, so it can be served from any path. This allows multiple apps to be simultaneously served on different paths. The example below uses the `get_asgi_app` function to obtain two separate Framework apps, which are then mounted on different paths, `/app1` and `/app2`, of a FastAPI app. ```py import uvicorn import writer.serve from fastapi import FastAPI, Response root_asgi_app = FastAPI(lifespan=writer.serve.lifespan) sub_asgi_app_1 = writer.serve.get_asgi_app("../app1", "run") sub_asgi_app_2 = writer.serve.get_asgi_app("../app2", "run") root_asgi_app.mount("/app1", sub_asgi_app_1) root_asgi_app.mount("/app2", sub_asgi_app_2) @root_asgi_app.get("/") async def init(): return Response("""

Welcome to the App Hub

""") uvicorn.run(root_asgi_app, host="0.0.0.0", port=5328, log_level="warning", ws_max_size=writer.serve.MAX_WEBSOCKET_MESSAGE_SIZE) ``` # Deploy with Docker To deploy on the Writer cloud see instructions [here](/framework/quickstart#deploying-on-writer-cloud). You can use Docker to deploy Framework anywhere. If you're an experienced Docker user, you may want to go straight to the provided Dockerfile. ## Creating a Docker image Make sure you have Docker installed on your system. Open a terminal and navigate to your app’s folder. Create a `pyproject.toml` file using `poetry init` and install `writer` using `poetry add writer`. A Dockerfile is a file with instructions that tell Docker how to build your image. It must be named `Dockerfile`. You can use the following as-is, or as a starting point. It should be saved in your app's folder, together with `main.py` and `.wf/`. ```docker FROM python:3.10-bullseye RUN apt-get update -y && mkdir /app RUN apt-get install build-essential cmake python3-dev -y COPY . /app WORKDIR /app RUN pip3 install poetry RUN poetry config virtualenvs.create false RUN poetry install --only main ENTRYPOINT [ "writer", "run" ] EXPOSE 8080 CMD [ ".", "--port", "8080", "--host", "0.0.0.0" ] ``` This Dockerfile is just a guideline. It uses an official Python slim base image with a multistage build to reduce the size of the built image. If you're a Docker expert, feel free to work on your own `Dockerfile`. Framework is, after all, a standard Python package. To build the image, use `docker build . -t ` followed by an image tag, which you're free to choose and will locally identify your image. ```sh docker build . -t my_framework_app ``` By default, Docker builds images in the architecture it's being run on. If you're working with an ARM computer, such as a Mac M2 or a Raspberry Pi, Docker will build an ARM image. Most cloud services will only accept x86 images. You can use another computer (or virtual machine) to build the image, or you can use [Docker buildx](https://docs.docker.com/build/building/multi-platform/). ## Publishing your Docker image Once your Docker image has been built, you can publish it to a registry. This is a place where Docker images are stored and made available to other services. We recommend using Docker Hub; it has a generous free tier, it's very easy to set up and it's widely supported. However, you can choose to use another service such as Azure Container Registry. To use Docker Hub, you'll need to sign up for an account. You can push your image using the following commands. ```sh # Login to Docker Hub docker login # Push the image # Replace "my_writer_app" for the tag you've previously chosen # Replace "my_user" for your user on Docker Hub docker tag my_writer_app:latest my_user/my_writer_app:latest docker push my_user/my_writer_app:latest ``` If your image is public, anyone can now run the following command and start the app. It's important to bind the port to a port in the host machine. The command below binds port 8080 in the Docker image to port 8080 in the host. ```sh docker run -p 8080:8080 my_user/my_framework_app ``` Go on your browser to [http://localhost:8080](http://localhost:8080) to check everything is working as expected. ## Deploying your Docker image As mentioned earlier, once the image is a registry, it can be spun up by others. After trying a few options, we recommend using Google Cloud Run. Its free tier is generous and SSL works out of the box. ![Run and Share - Google Cloud Run](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/deploy-with-docker.google-cloud-run.png) Cloud Run can be configured in just one page. It takes the image from a registry and makes it available via a URL, with SSL enabled by default. We recommend the following settings: Minimum 0 instances, maximum 4 instances. This range is suitable unless your app needs to serve several thousands of users. Set the request timeout to the maximum allowed and enable Session Affinity. This ensures that WebSocket connections are not unnecessarily dropped. Allocate 2GB of memory and 2 vCPUs. This configuration will likely be enough to comfortably run a simple app. However, you can probably manage with much less—512MB of memory and 1 vCPU—if your app isn’t too demanding and you don’t expect much traffic. # Event handlers Events originate in the front-end, for example, when a user clicks a *Button* component. Using the Builder, these events can be linked to event handlers. ## Plain Python functions Event handlers are Python functions accessible from `main.py`. They can be defined in that same file or imported. No decorators or special syntax are required. ```py # This event handler will add an entry to the log def handle_click() print("Hello") ``` To specify that a function isn't an event handler and should remain hidden to the front-end, prefix it with a `_` (underscore). ```py # This function won't be visible in the front-end # because its name starts with an underscore def _reticulate(splines): r_splines = np.random.normal(size=(splines,100)) return r_splines ``` ### External handlers If your `main.py` file has become cluttered with too many handler functions, you can organize them more effectively using the `init_handlers` method. This method allows you to register handler functions from other modules. You can pass a single imported module or a list of modules to the init\_handlers method to register multiple handlers simultaneously: ```py one_handler # my_app/my_handlers_module.py def increment(state): state["counter"] += 1 ``` ```py init_handlers one module # my_app/main.py import writer as wf import my_handlers_module wf.init_handlers(my_handlers_module) # Register all functions from the module as handlers; # this makes `increment` handler accessible on front-end ``` ```py init_handlers many modules # my_app/main.py import writer as wf import handler_module_one import handler_module_two wf.init_handlers([handler_module_one, handler_module_two]) ``` Each function inside a module is attempted to be registered as a handler. Make sure to use `_` prefix as described [before](#plain-python-functions) to prevent exposing unwanted functions to front-end. You can also call `init_handlers` within other modules, which allows for a sequence of registrations: ```py another_handlers_module # my_app/another_handlers_module.py def decrement(state): state["counter"] -= 1 ``` ```py register_additional_handlers # my_app/my_handlers_module.py import writer as wf import another_handlers_module wf.init_handlers(another_handlers_module) # Makes `decrement` handler accessible on front-end ... ``` ```py my_handlers_module # my_app/main.py import writer as wf import my_handlers_module ... ``` Note that for this "chain" to work, you need to import the final module in the sequence into `main.py`. ## Mutating state In most cases, event handlers will modify the application state. State can be accessed by including the `state` argument in the handler, which will provide you with a `WriterState` object for the session that invoked the handler. Elements of state can be reached using the square brackets syntax `state["my_element"]`. Accessing keys that don't exist will return `None`. ```py def handle_click(state): state["counter"] += 1 ``` The handler above receives the application state for the relevant session and mutates it. For example, if Bob's counter was 4, and he clicks on a *Button* linked to `handle_click`, his new counter value will be 5. Other sessions remain unaffected. ## Mutation detection Mutations are detected via assignment. Make sure you perform an assignment on the state element you're mutating, for the mutation to be detected. When communicating with the front-end, Framework only sends state elements that have mutated. To detect which elements have mutated, it relies on assignment (via operators such as `=`, `+=`, etc). This is because Python doesn't offer a performant, reliable mechanism to detect mutations. See the two examples below. ```python hande_click def handle_click(state): state["my_df"].sample(frac=1, random_state=random.seed()) # The self-assignment is necessary when mutating # an existing object directly on state state["my_df"] = state["my_df"] ``` ```python hande_click_cleaner # The following cleaner code also works as it relies on assignment def hande_click_cleaner(state): my_df = state["my_df"] my_df.sample(frac=1, random_state=random.seed()) state["my_df"] = my_df # State assignmnet ``` ## Mutation event You can subscribe to mutations on a specific key in the state. This is useful when you want to trigger a function every time a specific key is mutated. ```python simple subscription import writer as wf def _increment_counter(state): state['my_counter'] += 1 state = wf.init_state({"a": 1, "my_counter": 0}) state.subscribe_mutation('a', _increment_counter) state['a'] = 2 # trigger _increment_counter mutation ``` ```python multiple subscriptions import writer as wf def _increment_counter(state): state['my_counter'] += 1 state = wf.init_state({ 'title': 'Hello', 'app': {'title', 'Writer Framework'}, 'my_counter': 0} ) state.subscribe_mutation(['title', 'app.title'], _increment_counter) # subscribe to multiple keys state['title'] = "Hello Pigeon" # trigger _increment_counter mutation ``` ```python trigger event handler import writer as wf def _increment_counter(state, context: dict, payload: dict, session: dict, ui: WriterUIManager): if context['event'] == 'mutation' and context['mutation'] == 'a': if payload['previous_value'] > payload['new_value']: state['my_counter'] += 1 state = wf.init_state({"a": 1, "my_counter": 0}) state.subscribe_mutation('a', _increment_counter) state['a'] = 2 # increment my_counter state['a'] = 3 # increment my_counter state['a'] = 2 # do nothing ``` `subscribe_mutation` is compatible with event handler signature. It will accept all the arguments of the event handler (`context`, `payload`, ...). ## Receiving a payload Several events include additional data, known as the event's payload. The event handler can receive that data using the `payload` argument. For example, the `wf-change` event in a *Text Input* component is triggered every time the value changes. As a payload, it includes the new value. ```py def handle_input_change(state, payload): state["value"] = payload ``` The content of the payload will vary depending on the event. For example, when a user takes a photo with a *Webcam Capture*, the picture they took is sent across as a PNG image. ```py def handle_webcam_capture(payload): image_file = payload with open(f"picture.png", "wb") as file_handle: file_handle.write(image_file) ``` Handling different payloads across events can be challenging, especially since the shape of the payload may vary. To simplify this process, the Builder provides stub code that can help you get started with writing an event handler. You can access it by clicking the icon located next to the event when configuring the component's settings. This feature can help you quickly understand the structure of the payload and start writing the appropriate code to handle it. ## Globals You can use globals and module attributes, just as you would in a standard Python script. This is very convenient for storing a single copy of resource-intensive object. ```py my_ai = CatIdentifierAI() def evaluate(state, payload): result = my_ai.process(payload) state["is_a_cat"] = result ``` Take into account that globals apply to all users. If you need to store data that's only relevant to a particular user, use application state. ## Middlewares Middlewares are functions that run before and after every event handler. They can be used to perform tasks such as logging, error handling, session management, or modifying the state. ```py import writer as wf @wf.middleware() def middleware_before(state, payload, context): print("Middleware before event handler") state['running'] += 1 yield print("Middleware after event handler") state['running'] -= 1 ``` A middleware receives the same parameters as an event handler. A middleware can be used to handle exceptions that happens in event handlers. ```py import writer as wf @wf.middleware() def middleware_before(state): try: yield except Exception as e: state['error_counter'] += 1 state['last_error'] = str() finally: pass ``` ## Standard output The standard output of an app is captured and shown in the code editor's log. You can use the standard `print` function to output results. ```py # Shown every time the app starts print("Hello world") def payload_inspector(state, payload): # Shown every time the event handler is executed print("Payload: " + repr(payload)) ``` ## Execution flow Event handlers run in a thread pool and are non-blocking. Each event is processed independently from each other. State mutations are sent to the front-end after the function has finished executing. The code in `handle_fast` will accumulate all mutations and send to the front-end after the function returns. For long-running tasks, Framework will periodically check state and provide partial updates to the user. ```py handle_fast def handle_fast(state): state["text"] = "Hello" state["x"] += 3 state["y"] += 2 ``` ```py handle slowly # The code below will set `message` to "Loading...", then to "Completed". def handle_slowly(state): state["message"] = "Loading..." import time time.sleep(5) state["message"] = "Completed" ``` ## Asynchronous event handlers Framework supports asynchronous event handlers, allowing for non-blocking I/O operations directly within event handlers. This is particularly useful for tasks such as fetching data from a database, making HTTP requests, or performing any other I/O bound operation that can benefit from asynchronous execution. ### Defining an asynchronous handler An asynchronous event handler is defined with the standard `async` keyword syntax. ```py # An asynchronous event handler for performing an I/O bound operation async def handle_async_click(state): data = await fetch_data() state["data"] = data ``` In the example above, `fetch_data()` is an asynchronous function that retrieves data, potentially from a remote source. The `await` keyword is used to wait for the operation to complete without blocking the main thread, allowing other tasks to run concurrently. ### Awaitable objects You can use any awaitable object within an async event handler. This includes the output of any function defined with `async def`, or objects with an `__await__` method. This makes it easy to integrate with asynchronous libraries and frameworks. ## Context The `context` argument provides additional information about the event. The context provide the id of component that trigger the event in `target` field. ```py def handle_click(state, context: dict): last_source_of_click = context['target'] state["last_source_of_click"] = last_source_of_click ``` The context provides the event triggered in the `event` field. ```py def handle_click(state, context: dict): event_type = context['event'] if event_type == 'click': state["last_event"] = 'Click' ``` The repeater components have additional fields in the context, such as defined in `keyVariable` and `valueVariable`. ```py def handle_repeater_click(state, context: dict): key = context['keyVariable'] state['repeater_content'][key]['last_action'] = 'Clicked' ``` More information in [Repeater chapter](/framework/repeater) # Frontend scripts Framework can import custom JavaScript/ES6 modules from the front-end. Module functions can be triggered from the back-end. ## Importing an ES6 module Similarly to [stylesheets](/stylesheets), front-end scripts are imported via Framework's `mail` capability. This allows you to trigger an import for all or specific sessions at any time during runtime. When the `import_frontend_module` method is called, this triggers a dynamic [import()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import) call in the front-end. The `import_frontend_module` method takes the `module_key` and `specifier` arguments. The `module_key` is an identifier used to store the reference to the module, which will be used later to call the module's functions. The `specifier` is the path to the module, such as `/static/mymodule.js`. It needs to be available to the front-end, so storing in the `/static/` folder is recommended. The following code imports a module during event handling. ```py def handle_click(state): state.import_frontend_module("my_script", "/static/mymodule.js") ``` If you want the module to be imported during initialisation, use the initial state. ```py initial_state = wf.init_state({ "counter": 1 }) initial_state.import_frontend_module("my_script", "/static/mymodule.js") ``` Use versions to avoid caching. Similarly to stylesheets, your browser may cache modules, preventing updates from being reflected. Append a querystring to invalidate the cache, e.g. use `/static/script.js?3`. ## Writing a module The module should be a standard ES6 module and export at least one function, enabling it to be triggered from the back-end. As per JavaScript development best practices, modules should have no side effects. An example of a module is shown below. ```js let i = 0; export function sendAlert(personName) { i++; alert(`${personName}, you've been alerted. This is alert ${i}.`); } ``` ## Calling a function Once the module is imported, functions can be called from the back-end using the `call_frontend_function` method of state. This function takes three arguments. The first, `module_key` is the identifier used to import the module. The second, `function_name` is the name of the exported front-end function. The third, `args` is a `List` containing the arguments for the call. The following event handler triggers the front-end function defined in the section above. ```py def handle_click(state): state.call_frontend_function("mymodule", "sendAlert", ["Bob"]) ``` ## Import a JS script Framework can also import and run JavaScript scripts directly, for their side effects. These are imported via the report's `import_script` method. This method takes two arguments. The first, `script_key` is the identifier used to import the script. The second, `path` is the path to the file. The specified path must be available to the front-end, so storing it in your application's `./static` folder is recommended. ```py initial_state = wf.init_state({ "counter": 1 }) initial_state.import_script("my_script", "/static/script.js") ``` Prefer ES6 modules: importing scripts is useful to import libraries that don't support ES6 modules. When possible, use ES6 modules. The `import_script` syntax is only used for side effects; you'll only be able to call functions from the back-end using modules that have been previously imported via `import_frontend_module`. ## Importing a script from a URL Framework can also import scripts and stylesheets from URLs. This is useful for importing libraries from CDNs. The `import_script` and `import_stylesheet` methods take a `url` argument, which is the URL to the script or stylesheet. ```python initial_state = wf.init_state({ "my_app": { "title": "My App" }, }) initial_state.import_script("lodash", "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.js") ``` ## Frontend core Effectively using Framework's core can be challenging and will likely entail reading its [source code](https://github.com/writer/writer-framework/blob/master/ui/src/core/index.ts). Furthermore, it's considered an internal capability rather than a public API, so it may unexpectedly change between releases. You can access Framework's front-end core via `globalThis.core`, unlocking all sorts of functionality. Notably, you can use `getUserState()` to get values from state. ```js export function alertHueRotationValue() { const state = globalThis.core.userState.value. console.log("State is", state); } ``` # Handling inputs There are two, complementary, ways to handle inputs in Framework: via event handlers and via binding. ## Event handlers Input components have *change* events that are dispatched when the value changes. The new value is provided as a payload in the event handler. Change events have slightly different names across components, reflecting the payloads they provide. For example, *Number Input* and *Slider Input* use the event `wf-number-change` while *Text Input* and *Text Area Input* use the generic `wf-change`. As discussed in the [Event handlers](/framework/event-handlers) section, the payload can be accessed via the `payload` argument in the event handler. ```py # This event handler takes the payload and assigns it # to the state element "name" def handle_input_change(state, payload): state["name"] = payload ``` ## Two-way bindings Writing event handlers for every input component can be tedious. In most cases, you'll only need to update a single element of state when the value changes, akin to the example above. You can achieve this by binding a component to a state element. Bindings automatically handle the *change* event for the component and set the value of the state element to the payload. Furthermore, bindings are two-way. If the state element is updated from the back-end, the front-end component is updated to reflect the new value. As mentioned in the [Builder basics](/framework/builder-basics) section of the guide, bindings can be configured in the component settings. ![Repeater example](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/handling-inputs.binding.png) The binding above establishes a two-way link between the component and the state element `name`. If `name` changes in the back-end, the component changes. If the component changes, the value of `name` changes. ## Using events and bindings simultaneously Bindings can be used together with events. This is useful for triggering recalculations or applying dynamic filters. For example, you may want to have three *Number Input* components bound to `a`, `b` and `c` and display a value `n`. This easily done by binding the components and linking the same recalculation event handler to all three components. ```py def recalculate(state): state["n"] = state["a"]*state["b"]*state["c"] ``` ## Handling inputs safely Framework automatically sanitises the payloads it provides for its built-in events, those that start with `wf-`. For example, if a *Dropdown Input* component lists options `high` and `low`, you're guaranteed you won't get a value like `"Robert'); DROP TABLE students;--"` when handling `wf-option-change`. You'll get `"high"`, `"low"` or `None`. Inputs are sanitised, but you should still be careful As with any application, it's important to be familiar with the risks associated with handling user input, especially SQL injections. If you're using any custom HTML and mixing it with user generated content, make sure you understand XSS. ## Creating forms Input components can be combined with *Message* and *Button* components to create forms with messages, indicating whether the submission was successful. ![Form example](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/handling-inputs.form.png) # Introduction The Writer Framework lets you build feature-rich apps by using a drag-and-drop visual editor called **the Builder** and writing the back-end code in Python. It's fast and flexible, with clean, easy-to-test syntax. It provides separation of concerns between UI and business logic, enabling more complex apps. ![Framework Builder screenshot](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/public/builder.png) Build AI apps with the Writer Framework when: 1. You need to incorporate external data sources, such as external APIs 2. You have complex user input that requires custom logic, such as conditions that trigger the use of different prompts 3. You want to quickly analyze and visualize data using an LLM The Writer Framework offers: Define event handlers as plain Python functions. ```python def handle_text_update(state): state["text"] = "Updated text" writer.init_state({ "text": "Initial text" }) ``` Link the event handler and state to the UI seamlessly. Install with a simple pip command. Save user interfaces as JSON to be version controlled with the rest of the app. Use your local code editor with instant refreshes or the provided web-based editor. Edit the UI while your app is running without needing to click “Preview.” Event handling adds only 1-2ms to your Python code. Use WebSockets to synchronize front-end and back-end states. Non-blocking by default, with asynchronous event handling in a dedicated thread pool. No CSS required for customization like shadows, button icons, and background colors. Include HTML elements with custom CSS using the HTML Element component, which can serve as containers for built-in components. To get started, head to [Quickstart](/framework/quickstart) or our tutorials: Generate multiple social media posts in a click of button using our social media generator. Using Knowledge Graph, our graph-based RAG solution, you can build chat assistants to quickly ask questions using your data sources. Build real-time digital shelves for hundreds of products that are automatically customized for different e-retailers. # Page routes Framework apps can have multiple pages, with parametrised routes. Pages can be switched from the front-end or the back-end. ## Basic navigation To enable navigation between *Page* components, they must have a key assigned. This can be set up from the component's settings. Once a key is set up, the page will be accessible at `/#my_page_key`. ### Frontend-triggered page changes For basic page changes, assign a "Go to page" action to an event handler. For example, if you want to change to page `my_page_key` when a *Button* is clicked, go to the button's settings and under the `click` event select `Go to page "my_page_key"`. ### Backend-triggered page changes Trigger a page change from the back-end using the `set_page` method of state. ```py # This event handler sends the user to a different page # depending on time of the day def handle_click(state): from datetime import datetime now = datetime.now() if now.hour >= 18: state.set_page("fun_work_page") else: state.set_page("work_work_page") ``` ## Routes with parameters You may want to share a URL that links directly to a specific resource within your app. For example, to a specific location or product. You can do so by specifying parameters in the URL, known as route vars. Framework URLs contain the page key, followed by the route vars and their values. For example, `/#detailPage/product_id=32&country=AR`. ### Adding vars to the URL from the back-end You can set up variables that are displayed in the URL by passing a dictionary to the `set_route_vars` state method. Use `None` to clear specific keys. ```py # The following code will set the value of product_id # to the value of the "product" state element def change_route_vars(state): state.set_route_vars({ "product_id": state["product"] }) ``` ### Retrieving the values Framework uses the hash portion of the URL to store page and variable data, so even when switching pages or changing variables, the page doesn't reload. To monitor changes to the active URL, set up an event handler for `wf-hashchange` in the *Root* component. ```py # The following event handler reads the product_id route var, # then assigns its value to the "product" state element. def handle_hash_change(state, payload): route_vars = payload.get("route_vars") if not route_vars: return state["product"] = route_vars.get("product_id") ``` # Product description generator In this tutorial, you'll use the Writer Framework to build a Saturn Snacks product description generator for a variety of food outlets. After adding the initial functionality of the app, you'll also extend the app to include a chart of SEO keyword analysis and the ability for users to add their own food outlet. ![Finished application](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/product_desciption/pd_gen_1.png) ## Setting up your project ### Creating a Writer app and getting your API key From the Home screen, click on **Build an app**. ![Writer home screen](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/product_desciption/pd_gen_2.png) Select Framework as the app type you’d like to create, enabling you to generate keys and build your app with the Writer Framework. ![App type selection](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/product_desciption/pd_gen_3.png) On the next screen, you can edit your Writer application name in the upper left. Underneath “Authenticate with an API key,” click on “Reveal” to see and copy your API key. ### Creating the application Next, open your terminal and navigate to the directory where you want to create your application directory. To pass your API key to the Writer Framework, you'll need to set an environment variable called `WRITER_API_KEY`. Here’s how you can set this variable in your terminal session: ```sh On macOS/Linux export WRITER_API_KEY=[key] ``` ```sh On Windows set WRITER_API_KEY=[key] ``` Run the following command to create your application. Replace `product-description-app` with your desired project name and `pdg-tutorial` with the template you wish to use: ``` writer create product-description-app --template=pdg-tutorial ``` This command sets up a new project called `product-description-app` in the specified directory using a template designed for this tutorial. To edit your project, run the below commands. This will bring up the console, where Framework-wide messages and errors will appear, including logs from the API. By default, the Writer Framework Builder is accessible at `localhost:4005`. If that port is in use, you can specify a different port. Open this address in your browser to view your default application setup. ```bash Standard port writer edit product-description-app ``` ```bash Custom port writer edit product-description-app --port=3007 ``` ## Introduction to the application setup When you first start up the application, you're going to see two main layout items provided by the template: 1. A Header component with the name of the application 2. A Column container that'll house most of the UI of the app The left column includes a form that has three text inputs and a button. These three text inputs are bound to corresponding state elements. The right column contains a Message component for loading and status messages, as well as an empty Tab container which you'll use to display the product descriptions of the various outlets. ### Code overview Looking at the code in `main.py`, you'll see that the template already imported the Writer Framework, the AI module, and the product description prompts that you'll use throughout this tutorial. ```python import writer as wf import writer.ai from prompts import base_prompts, user_prompt, seo_keywords ``` The prompts are stored in a separate file called `prompts.py`. You are welcome to open this project in the IDE of your choice and modify the prompts however you wish. However, you don't need to make any changes to this file to follow this tutorial. You'll also see the state initialization: ```python wf.init_state({ "form": { "title": "", "description": "", "keywords": "" }, "message": "Fill in the inputs and click \"Generate\" to get started.", }) ``` The form elements and the message have been initialized as strings. You'll add to this state dictionary throughout the tutorial. ## Implementing the Product description generator Your first goal is to generate product descriptions for the various food outlets, with each outlet appearing as a tab to the right of the form. ![Finished product description tabs](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/product_desciption/pd_gen_4.png) ### Setting up the code First, integrate new functionality into your code for generating product descriptions. Paste the following method on line 5 after all of the imports to create a helper function for generating product descriptions: ```python def _generate_product_description(base_prompt, product_info): prompt = base_prompt.format(**product_info) description = writer.ai.complete(prompt) return description ``` This function, `_generate_product_description`, accepts a base prompt and the product information from a form on the page. The underscore at the beginning of its name indicates that it's a private method not exposed to the UI. Update the `wf.init_state()` to include a `product_description` dictionary with visibility control and outlets for descriptions: ```python wf.init_state({ "form": { "title": "", "description": "", "keywords": "" }, "message": "Fill in the inputs and click \"Generate\" to get started.", "product_descriptions": { "visible": False, "outlets": {} } }) ``` This setup includes a variable `visible` to control whether product description tabs are shown or hidden, and an empty dictionary `outlets` for storing descriptions. Paste the following method beneath `_generate_product_description` to handle button clicks and generate descriptions: ```python def handle_click(state): state["product_descriptions"]["visible"] = False # Loop through all the base prompts to generate versions tailored to each outlet for outlet, base_prompt in base_prompts.items(): state["message"] = f"% Generating product description for {outlet}..." product_description = _generate_product_description(base_prompt, state["form"].to_dict()) state["product_descriptions"]["outlets"][outlet] = product_description state["product_descriptions"]["visible"] = True state["message"] = "" ``` This handler will loop through each imported base prompt, format it with the form information, and pass it to the helper method. The handler also manages UI interactions, such as displaying and hiding product descriptions and managing loading messages. ### Setting up the user interface You can now set up the UI to iterate over the product descriptions dictionary and create tabs. Begin by opening the User Interface. In the toolkit, drag a Repeater component from the Other section into the empty Tab Container. Click on the Repeater component to open its component settings. Under Properties, add `@{product_descriptions.outlets}` as the Repeater object to be used for repeating the child components. Replace the default “Key variable name” with `itemID`. You can leave “Value variable name” as `item`. ![Repeater settings](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/product_desciption/pd_gen_5.png) From the Layout section of the toolkit, drag a Tab component into the Repeater. Click on the Tab to bring up the component settings and add `@{itemID}` to the Name property to display the outlet name on the tab. ![Tab settings](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/product_desciption/pd_gen_6.png) Drag a Text component from the Content section of the Toolkit into the Tab. Click on the Text component to open the Component settings and set the Text property to `@{item}`. You may also choose to set “Use Markdown” to “yes.” ![Text settings](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/product_desciption/pd_gen_7.png) Click on the Tab container to bring up its Component settings. Scroll to the bottom and, under Visibility, click “Custom” and add `product_descriptions.visible` to the Visibility value input. ![Tab container visibility settings](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/product_desciption/pd_gen_8.png) Click on the Generate button inside the form to bring up its Component settings. In the Events section, select `handle_click` for the `wf-click` event. ![Button settings](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/product_desciption/pd_gen_9.png) Click **Preview** in the top toolbar, enter some test data, and click the **Generate** button. You should see a loading message, as well as three example food outlets displayed in the tabs. The loading message should disappear when everything is loaded, and the tab should remain visible once the data has loaded. Great work! ## Expanding the application: SEO keyword analysis You can expand on this application by adding a chart that displays the top ten SEO keywords present in the product descriptions. ### Updating the code To do this, back in the code, first add the following helper function underneath your ` _generate_product_description` helper method: ```python def _generate_seo_keywords(outlets): combined_descriptions = "\n".join(f"{key}: {value}" for key, value in outlets.items()) # Generate the prompt with the provided descriptions prompt = seo_keywords.format(descriptions=combined_descriptions) # Strip out whitespace and backticks from the response return writer.ai.complete(prompt).strip(" `\n") ``` This method concatenates all of the product descriptions and incorporates them into a prompt in `prompts.py`. It then sends the formatted prompt to the Palmyra LLM using the `complete` method. The prompt not only analyzes the descriptions for SEO keywords, but also outputs a [Plotly.js](Plotly.js) schema object that you can use directly with a Plotly graph component. With the helper method in place, you can now update the click handler for the button. On line 27, add the following code before the product description visibility is set: ```python # Create the SEO analysis state["message"] = "Analyzing SEO keywords..." outlets = state["product_descriptions"]["outlets"] state["seo_analysis"] = _generate_seo_keywords(outlets) ``` This code sets the loading message and passes all of the product descriptions to the SEO keyword helper method. ### Adding SEO analysis to the UI To update the UI to display this chart, first drag a new tab from the Layout section of the toolkit into the Tab container. This tab should not be inside of the Repeater, but can be either before or after it. Click on the tab to open the component settings, and change the name to “SEO Analysis.” If you'd like, you can also set the Visibility to “Custom” and set `seo_analysis` as the visibility value. ![SEO Tab](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/product_desciption/pd_gen_10.png) To display the chart, drag a Plotly graph component from the Content section of the toolkit into your new tab. Click on the component to bring up the component settings. The Plotly graph component accepts a graph specification. Add `@{seo_analysis}` to pass the LLM-generated graph specification to the component. Click preview, add some data to the form, and click generate. You should see a new SEO analysis tab appear with a nicely formatted and labeled chart. ![SEO analysis tab and chart](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/product_desciption/pd_gen_11.png) ## Extending the application: user-added outlet Finally, you can extend this application even further by allowing users to add their own custom food outlet and derive a new description from a custom prompt. ### Adding the new form Start by building the UI for this new form. From the Layout section of the Toolkit, drag a new Section component into the column where the current form is and drop it above or below it. Click on the Section and change the Name to “Add an outlet.” To create the inputs for the form, drag a Text Input and a Number Input from the Input section of the Toolkit into the newly created section. Click on the Text Input component to change the Label to “Outlet name.” Click on the Number Input and change the label to “Character max.” Finally, add a Button from the Other section of the toolkit to the bottom of the new section. Click on the button and change the text to “Add and Generate.” You can also add `laps` or another [Material Symbols](https://fonts.google.com/icons) ID to the Icon input if you wish. ![Add outlet form](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/product_desciption/pd_gen_12.png) ### Updating the code In the code, you next need to add corresponding state elements for the new form components to `wf.init_state()`. Add the following to the state dictionary: ```python "outlet_form": { "name": "", "character_max": "", }, ``` Don't forget to check your commas when adding to the state dictionary. Your completed state should look like this: ```python wf.init_state({ "form": { "title": "", "description": "", "keywords": "" }, "outlet_form": { "name": "", "character_max": "", }, "message": "Fill in the inputs and click \"Generate\" to get started.", "product_descriptions": { "visible": False, "outlets": {} } }) ``` The `outlet_form` state elements will bind to the form elements. Next, add the click handler for the new button. Copy and paste this `handle_add_outlet` method into the code under the `handle_click` method: ```python def handle_add_outlet(state): # Create a new base prompt for the new outlet new_outlet_name = state["outlet_form"]["name"] product_info = {**state["outlet_form"].to_dict(), **state["form"].to_dict()} base_prompt = user_prompt.format(**product_info) # Add the new base prompt to the base_prompts dictionary base_prompts[new_outlet_name] = base_prompt # Generate the product description for the new outlet state["message"] = f"% Generating product description for {new_outlet_name}..." product_description = _generate_product_description(base_prompt, state["form"].to_dict()) state["product_descriptions"]["outlets"][new_outlet_name] = product_description # Update the SEO analysis state["message"] = "Updating SEO analysis..." outlets = state["product_descriptions"]["outlets"] state["seo_analysis"] = _generate_seo_keywords(outlets) state["message"] = "" ``` This method formats the input from both forms into the imported `user_prompt` and adds the formatted prompt to the `base_prompts` dictionary. It then generates the product description for the new food outlet, updates the SEO analysis, and clears the status message. ### Binding the elements and handler to the UI Finalize your setup by binding the state elements and configuring the click handler to the UI components. * **Outlet Name**: Click on the “Outlet name” Text Input component. In the Binding section of the component settings, set the state element to `outlet_form.name`. * **Character Max**: Move to the “Character max” Text Input. Update its state element binding to `outlet_form.character_max`. Click on the **Add and Generate** Button. In the Events section of the component settings, select `handle_add_outlet` for the `wf-click` event. To conditionally display the form based on whether descriptions have been generated, click on the Section containing the form. In the Visibility section, choose “Custom” and set `product_descriptions.visible` as the “Visibility value.” ### Testing the finished product To see the result of your hard work, click **Preview** in the top toolbar, enter some information into the original product description form, and click **Generate**. The “Add an outlet” form should appear once the product descriptions have been generated. Add a new example outlet name and a character max and click “Add and Generate.” You should see a new tab appear with your new outlet, as well as an updated SEO analysis chart. ![Finished application](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/product_desciption/pd_gen_13.png) You can add whatever additional form inputs you wish to the outlet form, but be sure to update `user_prompt` in the `prompts.py` file using your favorite editor. ## Deploying the application To deploy the application to the Writer cloud, either terminate your current Writer Framework process or open a new terminal session and run the following command: ``` writer deploy product-description-app ``` You’ll be prompted for your API key. Once the application is deployed, the CLI will return with the URL of your live application. ## Conclusion You’ve now built a full application with the Writer Framework and the Writer AI module. Congratulations! This application not only demonstrates the platform's capabilities but also provides a foundation on which you can build more complex applications. To learn more, explore the rest of the Writer Framework documentation and the API documentation. # Quickstart ## Overview In this guide, you'll learn how to get started with the Writer Framework by building a simple "Hello, World" application. This beginner-friendly guide will help you install the necessary tools, set up an app, and deploy it to the Writer Cloud. ## Step 1: Install Writer Framework and run the demo app Writer Framework works on Linux, Mac, and Windows. Python 3.9.2 or higher is required. We recommend using a virtual environment. Use the following command to install the Writer Framework using `pip`. Open your command line interface and type: ```bash pip install writer ``` This will download and install the Writer Framework and all its required dependencies. Once the installation is complete, you can verify that everything is set up correctly by running the built-in demo application. Navigate to your desired directory and type: ```bash writer hello ``` This command creates a subfolder named **hello** and launches the Writer Framework, opening a visual editor accessible through a local URL displayed in your command line. This demo app illustrates the different components available in Writer Framework, helping you get familiar with the setup. ## Step 2: Create a new framework app Now that we've tested the setup, let's create our first "Hello, World" app using the `ai-starter` template. This template showcases how quickly you can build AI-powered applications with the Writer Framework. Use the following command to generate the app structure: ```sh writer create hello-world --template=ai-starter ``` This will create a new app folder, **hello-world**, with the following structure: 1. `main.py` - The entry point for the app. You can import anything you need from here. 2. `.wf/` - This folder contains the UI component declarations. Maintained by the Writer Framework's visual editor. 3. `static/` - This folder contains front-end static files which you might want to distribute with your app, such as images and stylesheets. ## Step 3: Start the visual editor To start building and customizing your app visually, use the Writer Framework’s editor. This browser-based interface allows you to drag components, arrange layouts, and modify settings in real-time. ```sh writer edit hello-world ``` After running this command, you'll see a local URL displayed in your command line. Open this URL in a web browser to access the editor. In your terminal, enter the following command to open the editor: ```sh writer edit hello-world ``` This command opens the Writer Framework editor in your browser, where you’ll see a live preview of your app. The editor interface includes a **component library** on the left, a **canvas** in the center for building layouts, and a **settings panel** on the right for customizing selected components. The editor starts by default on port 4005. If you launch multiple editors in parallel and do not specify a port, Writer Framework will automatically assign the next port until reaching the limit of 4099. It's not recommended to expose the Framework to the Internet. If you need to access the editor remotely, we recommend setting up an SSH tunnel. By default, requests from non-local origins are rejected as a security measure to protect against drive-by attacks. If you need to disable this protection, use the flag `--enable-remote-edit`. On the left sidebar, browse through the **Component Library** and try dragging a few components onto the **Canvas**. For example, you can add a **Text** component to display "Hello, World!" or try other components to see how they work together. Experiment with arranging these components on the canvas to create a simple layout. With a component selected, look at the **Settings Panel** on the right side. Here, you can edit properties like text content, colors, and alignment to see how it changes the appearance and behavior of the component on the canvas. This panel allows you to customize components without writing any code, giving you a quick and visual way to build a frontend. To personalize your app further, open `main.py` in the root of your project folder. Locate the following line: ```python wf.init_state({ "my_app": { "title": "AI STARTER" }, }) ``` Change `"AI STARTER"` to something unique, like `"My First Writer App"`. Save the file, and you’ll see the updated name reflected immediately in the editor. For now, this simple exploration gives you an idea of how the framework enables rapid frontend building without code. In later tutorials, like creating a [Chat assistant](chat-assistant) or a [Social post generator](social-post-generator), you’ll explore more advanced components and layouts using this visual editor. ## Step 4: Run the app locally When your app is ready, execute the `run` command, which will allow others to run, but not edit, your Framework app. ```sh writer run hello-world ``` Your app starts by default on port 3005. If you launch multiple apps in parallel and do not specify a port, Writer Framework will automatically assign the next port until reaching the limit of 3099. You can specify a port and host. Specifying `--host 0.0.0.0` enables you to share your application in your local network. ```sh writer run hello-world --port 5000 --host 0.0.0.0 ``` ## Step 5: Deploy to Writer Cloud Writer provides a quick and fast way to deploy your apps via the [Writer Cloud](/framework/cloud-deploy). ```sh writer deploy hello-world ``` You’ll be asked to enter your API key. To find your key, log in to your [AI Studio account](https://app.writer.com/aistudio) and either create a new framework app by going through the create app workflow or choose an existing framework app from your home screen. For other deployment options, see [Deploy with Docker](/framework/deploy-with-docker). ## Conclusion Congratulations! You’ve set up, configured, and deployed your first app with the Writer Framework. Now, try building a [Chat assistant](chat-assistant) or a [Social post generator](social-post-generator) to put these skills into action. # Release notes generator In this tutorial, you'll build a release notes generator using the Writer Framework. This application will help you generate release notes as formatted HTML for software updates based on user-provided data as a CSV file. You can check out the [finished code on GitHub](https://github.com/writer/framework-tutorials/tree/main/release-notes-generator/end) to see what you'll be building. The application consists of two main parts: the backend, which processes the uploaded CSV file and generates the release notes, and the frontend, which displays the release notes and allows users to download them. ![Finished release notes generator application](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/release_notes/release_gen_1.png) ## Setting up your project ### Creating a Writer app and getting your API key From the Home screen, click on **Build an app**. ![Writer home screen](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/release_notes/release_gen_2.png) Select Framework as the app type you want to create, enabling you to generate keys and build your app with the Writer Framework. ![App type selection](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/release_notes/release_gen_3.png) On the next screen, you can edit your Writer application name at the top left. Underneath "Authenticate with an API key," click "Reveal" to view and copy your API key. ### Creating the application Next, open your terminal and navigate to the directory where you want to create your application directory. To pass your API key to the Writer Framework, you'll need to set an environment variable called `WRITER_API_KEY`. Here's how you can set this variable in your terminal session: ```sh On macOS/Linux export WRITER_API_KEY=[key] ``` ```sh On Windows set WRITER_API_KEY=[key] ``` Run the following command to clone the `framework-tutorials` repo and navigate to the folder containing the starting point for this application. ``` git clone https://github.com/writer/framework-tutorials.git cd framework-tutorials/release-notes-generator/start ``` To edit your project, run the following commands. This will bring up the console, which displays Framework-wide messages and errors, including logs from the API. By default, the Writer Framework Builder is accessible at `localhost:4005`. If this port is in use, you can specify a different port. Open this address in your browser to view your default application setup. ```bash Standard port writer edit . ``` ```bash Custom port writer edit . --port=3007 ``` ## Introduction to the application setup The template includes some basic code, UI setup, and files to help you get started. ### Included files The files `prompts.py` and `html_template.py` contain helper functions for generating prompts for the AI model and formatting the output HTML, respectively. In the `sample-input` folder, you'll find a sample CSV file that you can use to test the application. Finally, the `custom.css` file in the `static` folder contains custom CSS styles for the application. ### Dependency imports In `main.py`, you'll see that the dependencies are already imported at the top: ```python import writer as wf import writer.ai import pandas as pd from prompts import get_release_notes_summary_prompt, get_release_notes_desc_prompt, get_category_prompt from html_template import format_output ``` These dependencies include the Writer Framework, the Writer AI module, and pandas for data manipulation. ### Initial UI The template includes a basic UI setup, including a Page component with a Header component. The Header component also includes an Image. If you want to change the logo image, you can replace the `logo_image_path` variable in the state with the path to your desired image in the `static` folder. ## Initializing the application state First, in `main.py`, set up the initial state for the application. This state will store the application's title, logo image path, file data, metrics, and processing status for each step. You'll also import a custom CSS file to style the application and create a placeholder DataFrame. Create a placeholder DataFrame on the line above `wf.init_state`: ```python placeholder_data = { 'Description': ['Description 1', 'Description 2', 'Description 3'], 'Label': ['Label 1', 'Label 2', 'Label 3'] } initial_df = pd.DataFrame(placeholder_data) ``` Update the initial state for the application at the bottom of `main.py`: ```python initial_state = wf.init_state({ "my_app": {"title": "RELEASE NOTES GENERATOR"}, "logo_image_path": 'static/Writer_Logo_black.svg', "file": {"name": "", "file_path": ""}, "metrics": {"new_features": 0, "caveats": 0, "fixed_issues": 0, "total": 0}, "step1": { "raw_csv": initial_df, "completed": "no", "generate-button-state": "yes", "processing-message": None, "styled-table": "

csv table

" }, "step2": { "release-notes": None, "completed": "no", "formatted-release-notes": "notes should go here" }, }) ```
Import the custom CSS file below the initial state setup: ```python initial_state.import_stylesheet(".description, .list, .summary, .category ", "/static/custom.css") ```
## Building the file upload functionality First, you'll build the file upload feature. Note that `prompts.py`, `html_template.py`, and `custom.css` are provided in the starting point for the application. There is also a sample CSV file in the `sample-input` folder that you can use to test the application. ### Implementing the file upload handler To handle file uploads, you'll create a function in `main.py` that reads the uploaded CSV file, processes the data, and stores it in the application state. In `main.py`, create a function to handle file uploads. This function will read the uploaded CSV file, process the data, and store it in the application state. ```python def onchangefile_handler(state, payload): uploaded_file = payload[0] name = uploaded_file.get("name") state["file"]["name"] = name state["step1"]["processing-message"] = f'+File {name} uploaded successfully.' state["file"]["file_path"] = f"data/{name}" file_data = uploaded_file.get("data") with open(f"data/{name}", "wb") as file_handle: file_handle.write(file_data) data = pd.read_csv(state["file"]["file_path"]) df = pd.DataFrame(data) state["step1"]["raw_csv"] = df state["step1"]["generate-button-state"] = "no" ``` Define a function to convert the CSV file to a DataFrame: ```python def _raw_csv_to_df(state): data = pd.read_csv(state["file"]["file_path"]) df = pd.DataFrame(data) return df ``` ### Displaying the uploaded CSV file Next, you'll display the uploaded CSV file in the application UI. Add a Step Container component to the Page. This will contain the two steps for the application. Drag two Step components into the Step Container. Name the first one "Load CSV file" and the second "Release notes". Click on the the first Step component to select it and bring up the Properties pane. Set "Completed" to `@{step1.completed}`. This state reference will contain either "yes" or "no" based on the completion status of the step. Within this Step, add a Message component with the message set to `@{step1.processing-message}`. Scroll down to the Visibility section of the settings. Select "Custom" and set the condition to `step1.processing-message`. Add a Column Container component and add three Column components. For the first column, set the width to 0.5. For the third column, set "Content alignment (H)" to "Left" and "Content alignment (V)" to "Bottom." In the middle column, place a File Input component labeled "Please upload your CSV file". Set its `wf-file-change` handler to `onchangefile_handler`. In the third column, add a Button labeled "Generate release notes". Set its "Disabled" property to `@{step1.generate-button-state}` and its "Icon" property to `laps`. Under the columns, create a Section component and set the title to "Raw CSV". In this section, add a DataFrame component to display the raw CSV data. Configure its properties to use `@{step1.raw_csv}` as the data source. Toggle "Enable download," "Use Markdown," and "Wrap text" to "yes". Set the Separator color to `#d4b2f7` using the CSS tab. Your application should now look like this: ![Release notes generator UI](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/release_notes/release_gen_4.png) When using the sample data located in `sample-input/test-data.csv`, the Raw CSV section will display the uploaded CSV file: ![Raw CSV section](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/release_notes/release_gen_5.png) ## Generating release notes Now that you've set up the file upload functionality, you can generate release notes based on the uploaded CSV file. ### Defining text completion functions Using the prompts provided, define functions to get the category, release notes summary, and release notes description using AI completion. You'll use these functions to process the uploaded CSV file and generate release notes. Define a function to get the category using AI completion: ```python def _get_category(desc, label): prompt = get_category_prompt(desc, label) label = writer.ai.complete(prompt) return label ``` Define a function to get the release notes summary: ```python def _get_release_notes_summary(label, desc): prompt = get_release_notes_summary_prompt(label, desc) formatted_desc = writer.ai.complete(prompt) return formatted_desc ``` Define a function to get the release notes description: ```python def _get_release_notes_desc(label, desc): prompt = get_release_notes_desc_prompt(label, desc) formatted_desc = writer.ai.complete(prompt) return formatted_desc ``` ### Implementing the generate functionality You'll next implement the ability to process the CSV and generate release notes. Define a function to generate HTML for the categories: ```python def _create_df_for_category(df,state): unique_categories = df['Primary-Category'] formatted_output_list = list() for category in set(unique_categories): df_category = df[df['Primary-Category']==category] categories = {" New Feature": "new_features", " Caveat": "caveats", " Fixed Issue": "fixed_issues" } curr_category = categories[category] state["metrics"][curr_category]= df_category.shape[0] formatted_output = format_output(category,df_category) formatted_output_list.append(formatted_output) return "".join(formatted_output_list) ``` Define a function to write HTML to a file: ```python def _write_html_to_file(html): with open("data/output-html.html", "w") as file_handle: file_handle.write(html) ``` Next, create a function to handle the generate button click. This function will process the uploaded CSV file, generate release notes, and store the formatted output in the application state. ```python def handle_generate_button_click(state): state["step1"]["generate-button-state"] = "yes" state["step1"]["processing-message-isVisible"] = True state["step1"]["processing-message"] = "%Hang tight, preparing to process your file" notes_counter = 0 df = _raw_csv_to_df(state) csv_row_count = df.shape[0] for index, row in df.iterrows(): df.at[index,"Primary-Category"] = _get_category(label=row["Labels"], desc=row["Description"]) df.at[index,"Release-Notes-Summary"] = _get_release_notes_summary(label=row["Labels"], desc=row["Description"]) df.at[index,"Release-Notes-Description"] = _get_release_notes_desc(label=row["Labels"], desc=row["Description"]) notes_counter += 1 state["step1"]["processing-message"] = f'%Processing {notes_counter} of {csv_row_count} Release Notes' df_temp = df[["Primary-Category","Release-Notes-Summary","Release-Notes-Description"]] df_sorted = df_temp.sort_values(by='Primary-Category') state["step2"]["release-notes"] = df_sorted state["step1"]["completed"] = "yes" state["step1"]["processing-message"] = "" html = _create_df_for_category(df_sorted,state) _write_html_to_file(html) state["step2"]["formatted-release-notes"] = html state["metrics"]["total"] = df_sorted.shape[0] state["step1"]["generate-button-state"] = "no" ``` Finally, click on the "Generate release notes" button in the UI builder and set its `wf-click` handler to `handle_generate_button_click`. ## Displaying the release notes Now that you've generated the release notes, you can display them in the application UI. ### Implementing helper functions Define helper functions to handle back button clicks, write HTML to a file and download the HTML file. Define a function to handle the back button click: ```python def handle_back_button_click(state): state["step1"]["completed"] = "no" ``` Define a function to handle downloading the HTML file: ```python def handle_file_download(state): html_data = wf.pack_file("data/output-html.html","text/html") file_name = "output-html.html" state.file_download(html_data,file_name) ``` ### Building the initial release notes UI Next, you'll build the UI for the "Release notes" step. To display the Release notes Step component, you'll need to double-click on it. Inside of the Step component, add a Separator component. Below the Separator, add a Column Container component and a single Column component. In the column, add a Button. Set its text to "Back" and set its `wf-click handler` to `handle_back_button_click`. Set the "Icon" property to `arrow_back`. ### Buiding tabs for Release notes display Below the Back button, add a Tab Container component and two Tab components. Name them "Formatted release notes" and "Release notes". #### Formatted release notes tab In the first tab, you'll display the formatted release notes. In the first tab, add an HTML Element component. Set the "Element" property `div` and the "Styles" property to the following object: ``` { "padding": "16px", "min-height": "64px", "min-width": "64px", "border-radius": "8px", "background": "white", } ``` Finally, set the "HTML inside" property to `@{step2.formatted-release-notes}`. Inside thie HTML Element component, create a three-column layout using a Column Container component and three Column components. In each column, add three Metric components to display new features, caveats, and fixed issues, respectively. Set the "Name" of these components to a single space to remove the placeholder text: ` `. Then, set the values of these components to `@{metrics.new_features}`, `@{metrics.caveats}`, and `@{metrics.fixed_issues}`. Finally, set the "Note" text to "+New Features", "+Caveats", and "+Fixed Issues" respectively. The "+" sign will display styling that indicates a positive message. Under the columns, add a Button component. Set its "Text" to "Download HTML" and its "Icon" to `download`. Then, set the `wf-click` handler to `handle_file_download`. The Formatted release notes tab should look like this when using the sample data: ![Formatted release notes tab](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/release_notes/release_gen_1.png) #### Release notes tab Finally, you'll add a Dataframe component to the second tab to display the detailed release notes. In the second tab, start with a Metric component to show the total number of release notes generated. Set the "Name" to "Number of release notes generated" and the "Value" to `@{metrics.total}`. Delete the default value for "Note". Follow this with a DataFrame component to display the detailed release notes, setting the "Data" property to `@{step2.release-notes}`. Configure it for text wrapping, downloading, and searching capabilities. Set the Separator color to `#d4b2f7`. The final Release notes section should look like this: ![Release notes final UI](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/release_notes/release_gen_6.png) You can [see the finished code on GitHub](https://github.com/writer/framework-tutorials/tree/main/release-notes-generator/end) or in `framework-tutorials/release-notes-generator/end` in the `tutorials` repo you cloned at the beginning of the tutorial. ## Deploying the application To deploy the application to the Writer cloud, either terminate your current Writer Framework process or open a new terminal session and run the following command: ``` writer deploy . ``` You'll be prompted for your API key. Once the application is deployed, the CLI will return with the URL of your live application. ## Conclusion By following these steps, you've created a complete Release notes generator application using Writer Framework. To learn more, explore the rest of the Writer Framework documentation and the API documentation. # Repeater The *Repeater* component allows you to repeat a group of components according to a list or dictionary. ## How it works *Repeater* repeats its contents for every item of a given list or dictionary. It's similar to a `for each` construct. Each iteration is rendered with a different **context**, a dictionary containing the key and value for the relevant item. By default, in the variables `itemId` and `item`. ### Food Selector example ![Repeater example](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/repeater.example.png) Given the state below, the contents of *Repeater* will be repeated 3 times. For the first iteration, `itemId` will equal `Banana`, and `item` will equal `{"type": "fruit", "colour": "yellow"}`. Components inside *Repeater* will be able to access this data using references such as `@{itemId}` and `@{item.type}`. ```py wf.init_state({ "articles": { "Banana": { "type": "fruit", "colour": "yellow" }, "Lettuce": { "type": "vegetable", "colour": "green" }, "Spinach": { "type": "vegetable", "colour": "green" } }, "order_list": [] }) ``` ## Event context When working with *Repeater* components, you can get the context data using the `context` argument in the event handler. Continuing with the Food Selector example above, the following handler can be linked to a *Button* —allowing users to add items to a list. ```py # The event handler below adds the itemId # of the relevant article to the order list def handle_add_to_list(state, context): state["order_list"] += [context["itemId"]] ``` Binding directly to context isn't possible Use dynamic accessors when binding inside a *Repeater*. For example, `articles[itemId].colour`. This will bind to a different property of `articles` for every `itemId`. # Sample app library Writer Framework lets you build a wide variety of applications across different domains. Here are some sample apps to get you started, which are all [available on GitHub](https://github.com/writer/framework-tutorials/). Generate React apps editable in a code sandbox. Tool for analyzing financial content based on compliance guidelines. AI-powered interactive dashboard for summarizing and visualizing financial data and metrics. Check out the [app tour](https://writer.com/engineering/financial-app-writer-framework-palmyra-fin/) on our Engineering blog. Tool to generate promotional content based on uploaded demographic data. Uses Palmyra-Med to generate SOAP notes and extract ICD codes from patient-doctor chat. Use an embedded no-code chat app to chat with a Knowledge Graph about products. Tool for generating formatted product description pages. Utility for creating formatted, downloadable release notes from CSVs containing GitLab notes. [Full tutorial here](./release-notes-generator). # Seo and social sharing Writer Framework offers you the possibility of optimizing metadata to optimize your SEO and the sharing of information on social networks. ### Configure page title The page title is editable for web crawlers. This title is a key element for the SEO of your application. A bot will not load the app. It will see `Writer Framework` by default. ```python writer.serve.configure_webpage_metadata(title="My amazing app") ``` If you need dynamic title,you can use a function instead of a hard coded parameter. The title will be evaluated when the Robot loads the page. ```python def _title(): last_news = db.get_last_news() return f"Last news: {last_news.title}" writer.serve.configure_webpage_metadata(title=_title) ``` ### Configure meta tags http headers allow you to specify a title, a description and keywords which will be used by a search engine. *./server\_setup.py* ```python writer.serve.configure_webpage_metadata( title="My amazing app", meta={ "description": "my amazing app", "keywords": "WF, Amazing, AI App", "author": "Amazing company" } ) ``` You can also use a function to generate the meta tags dynamically. *./server\_setup.py* ```python def _meta(): last_news = db.get_last_news() return { "description": f"Last news: {last_news.title}", "keywords": f"{last_news.keywords}", "author": "Amazing company" } writer.serve.configure_webpage_metadata(meta=_meta) ``` ### Configure social networks When you share a link on social networks, they will try to fetch the metadata of the page to display a preview. *./server\_setup.py* ```python writer.serve.configure_webpage_metadata( opengraph_tags= { "og:title": "My App", "og:description": "My amazing app", "og:image": "https://myapp.com/logo.png", "og:url": "https://myapp.com" } ) ``` You can also use a function to generate the opengraph tags dynamically. *./server\_setup.py* ```python def _opengraph_tags(): last_news = db.get_last_news() return { "og:title": f"Last news: {last_news.title}", "og:description": f"{last_news.description}", "og:image": f"{last_news.image}", "og:url": f"https://myapp.com/news/{last_news.id}" } writer.serve.configure_webpage_metadata(opengraph_tags=_opengraph_tags) ``` # Sessions Sessions are designed for advanced use cases, being most relevant when Framework is deployed behind a proxy. ## Session information in event handlers You can access the session's unique id, HTTP headers and cookies from event handlers via the `session` argument —similarly to how `state` and `payload` are accessed. The data made available here is captured in the HTTP request that initialised the session. The `session` argument will contain a dictionary with the following keys: `id`, `cookies` and `headers`. Values for the last two are themselves dictionaries. ```py # The following will output a dictionary # with the session's id, cookies and headers. def session_inspector(session): print(repr(session)) ``` This enables you to adapt the logic of an event to a number of factors, such as the authenticated user's role, preferred language, etc. ## Session verifiers You can use session verifiers to accept or deny a session based on headers or cookies, thus making sure that users without the right privileges don't get access to the initial state or components. Session verifiers are functions decorated with `wf.session_verifier` and are run every time a user requests a session. A `True` value means that the session must be accepted, a `False` value means that the session must be rejected. ```py import writer as wf # Users without the header x-success will be denied the session @wf.session_verifier def check_headers(headers): if headers.get("x-success") is None: return False return True ``` # Social post generator In this tutorial, you'll use the Writer Framework to build an AI-powered tool for generating social media posts and tags based on the input you provide! The process will take only minutes using a drag-and-drop visual editor to build the user interface and Python for the back-end code. Here's what the finished project will look like: ![Finished social post generator project](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2ab.png) ## Prerequisites Before starting, ensure you have: * **A Writer account:** You don't need an account to use Writer Framework, but you'll need one to use the AI module. [Fortunately, you can sign up for an account for free!](https://app.writer.com/aistudio/signup) * **Python 3.9.2 or later**: Use the installer from [python.org](https://www.python.org/downloads/). * **pip:** This command-line application comes with Python and is used for installing Python packages, including those from Writer. * **A basic understanding of Python:** You should be familiar with the basics of the language. * **Your favorite code editor (optional):** There's a code editor built into Writer for writing back-end code, but you can also use Visual Studio Code, Notepad++, Vim, Emacs, or any text editor made for programming if you prefer. ## Setting up your project ### Create a Writer app and get its API key First, you'll need to create a new app within Writer. Log into Writer. From the Home screen, click on the **Build an app** button. ![Writer home screen](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2.png) The **Start building** menu will appear, presenting options for the types of apps you can create. Select **Framework**, located under **Developer tools**. This will create a brand new app based on Writer Framework. !["Start building" menu](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_3.png) On the next screen, titled **How to deploy an application**, you can get the API key for the app by clicking on the **Reveal key** button, located under the text **Authenticate with an API key**. Your complete API key will be displayed, and a "copy" button will appear. Click this button to copy the key; you'll use it in the next step. !["How to deploy an application" page](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2a.png) ### Set up your computer and create the app's project The next step is to set up the Writer Framework environment on your computer. You'll do this by creating a directory for the project, installing dependencies, and creating the project for the application using a template. Open your terminal application. On macOS and Linux, this application goes by the name *Terminal*; on Windows, you can use either *Windows PowerShell* (which is preferred) or *Command Prompt*. If you already have the `writer` and `python-dotenv` packages installed on your computer, you can skip this step. Install the `writer` and `python-dotenv` packages by entering the following commands in your terminal application: ``` pip install writer python-dotenv ``` This command tells `pip`, the Python package installer, to install two packages: * `writer`, which provides some command-line commands and enables Python code to interact with Writer and the Writer Framework. * `python-dotenv`, which makes it easy to manage environment variables by loading them from a `.env` file. This one is optional for this exercise, but you might find it useful when working on larger projects. To pass your API key to the Writer Framework, you need to set an environment variable called `WRITER_API_KEY`. Select your operating system and terminal application below, then copy and paste the command into your terminal application, replacing `[your_api_key]` with the API key you copied earlier: ```sh macOS/Linux (Terminal) export WRITER_API_KEY=[your_api_key] ``` ```sh On Windows (Windows PowerShell) $env:WRITER_API_KEY=[your_api_key] ``` ```sh On Windows (Command Prompt) set WRITER_API_KEY=[your_api_key] ``` The `WRITER_API_KEY` environment variable will remain defined as long your terminal session is open (that is, until you close your terminal application’s window). Create the project by entering this command into your terminal application: ``` writer create social-post-generator --template=ai-starter ``` This command sets up a new project called `social-post-generator` using a starter template called `ai-starter` so that you're not starting "from scratch." ## Build the UI Now that you've created the project, it's time to define the UI. The Writer Framework's drag-and-drop capabilities make it easy — even if you haven't done much UI work before! The project editor is a web application that runs on your computer and enables you to define and edit your app's user interface. Launch it by typing the following into your terminal application: ``` writer edit social-post-generator ``` You'll see a URL. Control-click it (command-click on macOS) to open it, or copy the URL and paste it into the address bar of a browser window. The browser window will contain the project editor, which will look like this: ![Project editor](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2b.png) You'll see the following: * The **canvas** is in the center. It displays the app's user interface. * The column on the left contains: * The **Core toolkit**, which contains all the UI components. You define the user interface by dragging components from the Toolkit and placing them on the canvas. * The **Component tree**, which shows the arrangement of the UI components on the canvas. It's also useful for selecting items on the canvas, especially when it has a lot of UI components. It's time to build the UI! Select the **Header** component by clicking it — it's the component at the top, containing the title **AI STARTER** and a gray area labeled **Empty Header**. When you click it, you'll see the **properties** panel appear on the right side of the page. This lets you view and edit the properties of the selected component. ![The selected header and its properties panel](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2c.png) The first property you'll see in the panel is the **Text** property, which defines the text that appears as the header's title. It should contain the value `@{my_app.title}`. The `@{` and `}` indicate that `my_app.title` is a variable and that its contents should be the text displayed instead of the literal text "my\_app.title". You'll set the value of this variable soon. Select the **Section** component by clicking it — it's just below the **Header** component and contains the title **Section Title** and a gray area labeled **Empty Section**. In the **properties** panel, clear out the value of the **Title** property. This will remove the *Section*'s default title. ![The selected section and its properties panel](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2d.png) The user will need a place to enter words or phrases that the app will use as the basis for generating posts and tags. Drag a **Text Input** component — and note, it's **Text *Input***, not **Text** — from the **Core toolkit** panel on the left (it's under **Input**, and you may need to scroll down a little to find it) and into the **Section**. Sections can act as containers for other components. You can search for a specific component by using the search bar at the top of the **Core toolkit** panel. Select the **Text Input** component. In the **properties** panel: * Find the **Label** property and set its value to `Topic for social posts and tags`. * (Optional) Feel free to add some placeholder to the *Text Input* component by setting the value of the **Placeholder** property with something like `Enter words or phrases describing your topic`. ![The text input component and its properties panel](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2e.png) Drag a **Button** component from the **Core toolkit** panel (it's under **Other**, and you may need to scroll down a little to find it) into the **Section**, directly below the **Text Input**. The user will click this button to submit their prompt. Select the **Button**. In the **properties** panel: * Set the **Text** property's value to `Generate posts`. * Find the **Icon** property, and set its value to `arrow_forward`. ![The button component and its properties panel](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2f.png) The process of creating social media posts and tags takes a few moments. In order to reassure the user that the app is working and hasn't crashed, it will use a **Message** component to display something reassuring while it's generating. Drag a **Message** component from the **Core toolkit** panel into the **Section** positioning it immediately below the **Button**. Select the **Message** component. In the **properties** panel: * Scroll down to the **Style** section and look for the **Loading** property, which sets the color of the **Message** component when it's loading. * Click its **CSS** button, which will cause a text field to appear below it. * Enter this color value into the text field: `#D4FFF2`. ![The message component and its properties panel](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2g.png) The **Section** that you were working on is for user input. Let's add a new **Section** to hold the output — the social media posts and tags the app will generate. Drag a **Section** component from the **Toolbox** panel and place it *inside* the **Section** that's already there, just below the **Message** component. That's right — **Sections** can contain other **Sections**! ![The new section inside the existing section](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2h.png) Select the **Section** you just added. In the **properties** panel: * Find the **Title** property and clear it its value to remove the **Section**'s title. * Scroll down to the **Style** section and look for the **Container background** property, which sets the **Section**'s background color. * Click its **CSS** button, which will cause a text field to appear below it. * Enter this color value into the text field: `#F6EFFD`. ![The new section and its properties](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2i.png) Writer Framework has a number of useful components to make your apps more functional and beautiful. One of these is the **Tags** component, which can take a list of hashtags (or words, or short phrases) and display them inside colorful "bubbles" to make them stand out. This app will display the social media tags it generates in a **Tags** component. Drag a **Tags** component from the **Toolbox** panel and place it inside the new **Section**. ![The tags component](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2j.png) Drag a **Separator** component from the **Toolbox** panel and place it inside the new **Section**, just below the **Tags** component. This will separate the tags from the posts. ![The separator](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2k.png) Finally, drag a **Text** component from the **Toolbox** panel and position it below the **Separator**. This will hold the generated social media posts. ![The text component](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2l.png) ## Add the back-end code With the UI laid out, it's time to work on the logic behind it. The logic behind the user interface is defined in a file named `main.py`, which is in your project's directory. This file was automatically generated; you'll update the code in it to define the behavior of your app. The simplest way to edit `main.py` is within the project editor. Click on the "toggle code" button (beside the word **Code**) near the lower left corner of the project editor page. ![Project editor with arrow pointing to toggle code button](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2m.png) A pane with the name **Code** will appear at the bottom half of the screen, displaying an editor for the the contents of `main.py`. ![Project editor with the code editor displayed](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2n.png) If you'd rather use a code editor instead of coding in the browser, use it to open the `main.py` file in your project's directory. Now follow these steps: You should see the following at the start of the file: ```python import writer as wf import writer.ai ``` Replace that code with the following: ```python import os import re import writer as wf import writer.ai # Set the API key wf.api_key = os.getenv("WRITER_API_KEY") ``` This code imports the libraries that the application will need and then reads your Writer Framework API key in the `WRITER_API_KEY` environment variable. When the user presses the app's **Button**, the app needs to call a function to generate and display the social media posts and tags. Find these comments in the code... ```python # Welcome to Writer Framework! # This template is a starting point for your AI apps. # More documentation is available at https://dev.writer.com/framework ``` ...and replace them with the following function: ```python def generate_and_display_posts_and_tags(state): print(f"Here's what the user entered: {state['topic']}") # Display message state["message"] = "% Generating social media posts and tags for you..." # Generate and display social posts prompt = f"You are a social media expert. Generate 5 engaging social media posts about {state['topic']}. Include emojis, and put a blank line between each post." state["posts"] = writer.ai.complete(prompt) print(f"Posts: {state['posts']}") # Generate and display hashtags prompt = f"You are a social media expert. Generate around 5 hashtags about {state['topic']}, delimited by spaces. For example, #dogs #cats #ducks #elephants #badgers" pattern = r"#\w+" hashtags = re.findall(pattern, writer.ai.complete(prompt)) state["tags"] = {item: item for item in hashtags} print(f"Tags: {state['tags']}") # Hide message state["message"] = "" ``` The `%` at the start of the string being assigned to `state["message"]` will be replaced by a “spinning circle” progress indicator graphic in the *Message* component. The `pattern` variable in the `# Generate and display hashtags` section defines a regular expression pattern to search for words that begin with the `#` character. The `r` in front of the opening quote specifies that the string is a *raw string*, which means that the `\` character should be treated as a literal backslash and not as the start of an escape character sequence. Note that `generate_and_display_posts_and_tags()` uses `print()` functions for debugging purposes, and you can use them to get a better idea of what's happening in the function. You'll see their output in both your terminal application and in the project editor's 'log' pane (which will be covered shortly) as you use the social post generator. This output will include: * The topic the user entered * The posts generated by the LLM * The hashtags generated by the LLM The `print()` functions don't affect the operation of the social post generator in any way, and you can remove them if you wish. The final step is to set the application's initial state. Find this code, which should be just after the `generate_and_display_posts_and_tags()` function... ```python # Initialise the state wf.init_state({ "my_app": { "title": "AI STARTER" }, }) ``` ...and replace it with this: ```python # Initialize the state wf.init_state({ "topic": "writing", "message": "", "tags": {}, "posts": "", "my_app": { "title": "SOCIAL POST GENERATOR" } }) ``` The Writer Framework's `init_state()` method sets the initial value of `state`, a dictionary containing values that define the state of the application. The key-value pairs in `state` are how you store values used by your app and how you pass data between the back-end code and the UI. The code above sets the initial value of `state` so that it has these key-value pairs: * `topic`: A string containing the topic that the application should generate social media posts and tags for. You'll bind its value to the *Text Input* component where the user will enter the topic. * `message`: A string containing text of the message that will be displayed to the user while the application is generating posts and tags. You'll bind its value to the **Message** component. * `tags`: A list containing the hashtags generated by the LLM. You'll bind its value to the **Tags** component. * `posts`: A string containing the social media posts generated by the LLM. You'll bind its value to the **Text** component. * `my_app`: A dictionary containing values that define the application's appearance. This dictionary has a single key-value pair, `title`, which defines the text that appears as the application's title. For more details about the `state` variable, see our [*Application state*](https://dev.writer.com/framework/application-state#application-state) page. That’s all the code. If you edited the code in the browser, save it by clicking the “save” button near the top right corner of the code editor. ![Project editor and code editor, with arrow pointing to save button](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2o.png) Click the "toggle code" button to hide the code editor. ## Bind the UI to the back-end code You've built the UI and written the code behind it. Let's connect the two! Go back to the browser window with the project editor and do the following: Earlier, you saw that the **Header** component's **Text** property was set to `@{my_app.title}`, a value in the app's `state` variable. You changed this value when you update the call to the Writer Framework's `init_state()` method. Select the **Text Input** component. In the **properties** panel, scroll down to the **Binding** section and find the **State element** property. This is where you specify the `state` variable key whose value will be connected to the **Text Input** component. Set its value to `topic`. ![Updating the text input component's state element property](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2p.png) Select the **Button** component. In the **properties** panel, scroll down to the **Events** section and find the **`wf-click`** property. This is where you specify the function to call when the user clicks the button — set its value to `generate_and_display_posts_and_tags`. ![Updating the button's wf-click property](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2q.png) Select the **Message** component. In the **properties** panel, find the **Message** property, which specifies the content of the **Message** component. Set its value to `@{message}`. ![Updating the message's message property](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2r.png) Select the **Tags** component. In the **properties** panel: * Find the **Tags** property, which specifies the source of the tags that the component will display. * Click its **JSON** button. * In the text field below the **JSON** button, set the **Tags** property's value to `@{tags}`. ![Updating the tags component's tags property](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2s.png) Select the **Text** component. In the **properties** panel: * Find the **Text** property, which specifies the content of the **Text** component. Set its value to `@{posts}`. * Set the **Use Markdown** property to **yes**. ![Updating the text component's properties](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2t.png) Select the **Section** component containing the **Tags** and **Text** components. In the **properties** panel: * Scroll to the **Visibility** property at the bottom. * Click on the **Custom** button. * In the **Visibility value** field, set the value to `posts`. This will cause the **Section** to be visible only when the `state` variable's `posts` key has a non-empty value. ![Updating the inner section's visibility property](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2u.png) ## Test the application You've completed all the steps to make a working social post generator, and you can try using it right now, even while editing the user interface! Enter a topic into the **Topic for social posts and tags** text field, then click the **Generate Posts** button\* *twice* — the first time will cause the **properties** panel to appear, and the second click will register as a click. You'll know that you've clicked the button when you see the **Message** component display the text “Generating social media posts and tags for you...” ![Waiting for the generator to finish while the message component displays its message](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2v.png) ...and soon after that, you should see some results: ![The results](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2w.png) To get a better sense of what the experience will be like for the user, switch to the preview by changing the edit mode (located near the upper left corner of the page) from *UI* mode to *Preview* mode by selecting the **Preview** option: ![The project editor with an arrow pointing to the Preview button](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2x.png) Here’s what the app looks like in *Preview* mode: ![The working social post generator, with the project editor in "Preview" mode](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2y.png) You can see the output of any `print()` functions and error messages by clicking on the **Log** button located near the upper right corner of the page: ![The social post generator with an arrow pointing to the Log button](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2z.png) Here’s what the app looks like when displaying the log: ![The social post generator, with the log pane displayed](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2aa.png) It's very helpful to be able to test the application while editing it. As you continue to work with Writer Framework, you'll find yourself alternating between making changes to your application and testing those changes without having to leave the project editor. ## Run the application locally Once you've tested the application, it's time to run it locally. Switch back to your terminal application. Stop the editor with ctrl-c, then run the application by entering the following command: ``` writer run social-post-generator ``` Note that the command starts with `writer run` as opposed to `writer edit`. This launches the application as your users will see it, without any of the editing tools. Even though you can preview your applications in the project editor, it's still a good idea to test it by running it on your computer, outside the project editor, before deploying it. You'll be able to access the application with your browser at the URL that appears on the command line. It should look like this: ![Finished social post generator project](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/tutorial/social_post/sp_gen_2ab.png) The Writer editor, which you launched with `writer edit social-post-generator`, and your application, which you launched with `writer run social-post-generator`, run on the same URL, but on different *ports* (specified by the number after the `:` character at the end of the URL). ## Deploy the app to the Writer Cloud (optional) Right now, the app will only run on your computer. To make it available online, you'll need to deploy it to the Writer Cloud. In your terminal application, stop your app with ctrl-c, then deploy your application by entering the following command: ``` writer deploy social-post-generator ``` You'll be asked to enter your app's API key. Once you do that, the Writer command-line application will start deploying your application to the Writer Cloud. The process should take a couple of minutes. Once the app has been deployed to the Writer Cloud, you'll be shown the URL for your application, which you can use to access it online. ## Conclusion That's it — you've built a functional social post generator using the Writer Framework! Feel free to modify this project! The Writer platform is flexible enough for you to customize, extend, and evolve your application into something completely different! To find out what else you can do, check out the documentation for [Writer Framework](https://dev.writer.com/framework/introduction) and the [Writer API](https://dev.writer.com/api-guides/introduction). # State schema Schema declarations on the [Application state](./application-state) allows Framework to handle complex serialisation scenarios, while also allowing your IDE and toolchains to provide autocomplete and type checking. ## Schema declaration ```python import writer as wf class AppSchema(wf.WriterState): counter: int initial_state = wf.init_state({ "counter": 0 }, schema=AppSchema) # Event handler # It receives the session state as an argument and mutates it def increment(state: AppSchema): state.counter += 1 ``` Accessing an attribute by its key is always possible. ```python def increment(state: AppSchema): state["counter"] += 1 ``` Attributes missing from the schema remain accessible by their key. ```python initial_state = wf.init_state({ "counter": 0, "message": None }, schema=AppSchema) def increment(state: AppSchema): state['message'] = "Hello pigeon" ``` ## Schema composition Schema composition allows you to model a complex Application state. ```python class MyappSchema(wf.State): title: str class AppSchema(wf.WriterState): my_app: MyappSchema counter: int initial_state = wf.init_state({ "counter": 0, "my_app": { "title": "Nested value" } }, schema=AppSchema) ``` ## Calculated properties Calculated properties are updated automatically when a dependency changes. They can be used to calculate values derived from application state. ```python class MyAppState(wf.State): counter: List[int] class MyState(wf.WriterState): counter: List[int] @wf.property(['counter', 'app.counter']) def total_counter(self): return sum(self.counter) + sum(self.app.counter) initial_state = wf.init_state({ "counter": 0, "my_app": { "counter": 0 } }, schema=MyState) ``` ## Multi-level dictionary Some components like *Vega Lite Chart* require specifying a graph in the form of a multi-level dictionary. A schema allows you to specify that an attribute which contains a dictionary must be treated as a dictionary, rather than as a group of state. ```python class AppSchema(wf.WriterState): vegas_graph: dict # Without schema, this handler is execute only once def handle_vega_graph(state: AppSchema): graph = state.vega_graph graph["data"]["values"][0]["b"] += 1000 state.vega_graph = graph initial_state = wf.init_state({ "vegas_graph": { "data": { "values": [ {"a": "C", "b": 2}, {"a": "C", "b": 7}, {"a": "C", "b": 4}, {"a": "D", "b": 1}, {"a": "D", "b": 2}, {"a": "D", "b": 6}, {"a": "E", "b": 8}, {"a": "E", "b": 4}, {"a": "E", "b": 7} ] }, "mark": "bar", "encoding": { "x": {"field": "a", "type": "nominal"}, "y": {"aggregate": "average", "field": "b", "type": "quantitative"} } }, }, schema=AppSchema) ``` ## Type checking A schema allows you to check the integrity of your back-end using the type system. The code below will raise an error with mypy. ```bash $ mypy apps/myapp/main.py apps/myapp/main.py:7: error: "AppSchema" has no attribute "countr"; maybe "counter"? [attr-defined] ``` Here is the code, can you spot the error ? ```python import writer as wf class AppSchema(wf.WriterState): counter: int def increment(state: AppSchema): state.countr += 1 initial_state = wf.init_state({ "counter": 26, }, schema=AppSchema) ``` # Stylesheets The appearance of your application can be fully customised via CSS stylesheets. These are dynamically linked during runtime and can be switched from the back-end, targeting all or specific sessions. ## Importing a stylesheet Stylesheet imports are triggered via Framework's `mail`, similarly to other features discussed in [Backend-initiated actions](/framework/backend-initiated-actions). When the import is triggered, the front-end downloads the specified stylesheet and creates a `style` element with its contents. The `import_stylesheet` method takes the `stylesheet_key` and `path` arguments. The first works as an identifier that will let you override the stylesheet later if needed. The second is the path to the CSS file.The path specified needs to be available to the front-end, so storing it in the `/static` folder of your app is recommended. The following code imports a stylesheet when handling an event. ```py def handle_click(state): state.import_stylesheet("theme", "/static/custom.css") ``` In many cases, you'll want to import a stylesheet during initialisation time, for all users. This is easily achievable via the initial state, as shown below. ```py initial_state = wf.init_state({ "counter": 1 }) initial_state.import_stylesheet("theme", "/static/custom.css") ``` Use versions to avoid caching. During development time, stylesheets may be cached by your browser, preventing updates from being reflected. Append a querystring to bust the cache, e.g. use `/static/custom.css?3`. ## Applying CSS classes You can use the property *Custom CSS classes* in the Builder's *Component Settings* to apply classes to a component, separated by spaces. Internally, this will apply the classes to the root HTML element of the rendered component. ![Stylesheets - Component Settings](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/stylesheets.component-settings.png) ## Tips for effective stylesheets The CSS code for the class used earlier, `bubblegum`, can be found below. Note how the `!important` flag is used when targetting style attributes that are configurable via the Builder. If the flag isn't included, these declarations will not work, because built-in Framework styling is of higher specificity. ```css .bubblegum { background: #ff63ca !important; line-height: 1.5; transform: rotate(-5deg); } /* Targeting an element inside the component root element */ .bubblegum > h2 { color: #f9ff94 !important; } ``` Component structure may change. When targeting specific HTML elements inside components, take into account that the internal structure of components may change across Framework versions. Alternatively, you can override Framework's style variables. This behaves slightly differently though; style variables are inherited by children components. For example, if a *Section* has been assigned the `bubblegum` class, its children will also have a pink background by default. ```css .bubblegum { --containerBackgroundColor: #ff63ca; --primaryTextColor: #f9ff94; line-height: 1.5; transform: rotate(-5deg); } ``` The class can be used in *Component Settings*. If the stylesheet is imported, the effect will be immediate. In case the stylesheet has been modified since imported, it'll need to be imported again. ![Stylesheets - Applied Classes](https://mintlify.s3.us-west-1.amazonaws.com/writer/framework/images/stylesheets.applied-classes.png) ## Targeting component types Framework components have root HTML elements with a class linked to their type. For example, *Dataframe* components use the class *CoreDataframe*. When writing a stylesheet, you can target all *Dataframe* components as shown below. ```css .CoreDataframe { line-height: 2.0; } ``` ## Implementing themes It's possible to switch stylesheets during runtime, by specifying the same `stylesheet_key` in subsequent calls. This allows you to implement a "theme" logic if desired, targeting the whole or a specific part of your app. ```py def handle_cyberpunk(state): state.import_stylesheet("theme", "/static/cyberpunk_theme.css") def handle_minimalist(state): state.import_stylesheet("theme", "/static/minimalist_theme.css") ``` # Testing Testing a Framework application is easy. Given that event handlers are plain Python functions that take arguments such as `state` and `payload`, you can inject your own and test whether the outcome is correct. This section will use `pytest` examples. ## State ### Accessing the initial state To get started, import your app's entry point, `main`. This will initialise state and make event handlers available. The initial state is available in the module, at `main.wf.initial_state` provided you imported `writer` as `wf`. ### Creating states For testing purposes, you can create your own state using the `WriterState` class in `writer.core`. Pass a dictionary when constructing it. ```py from writer.core import WriterState artificial_state = WriterState({ "a": 3, "b": 6 }) ``` ## Example The code of a Framework application basically consists of two things: 1. Initial state 2. Event handlers It's straightforward to test both, as shown below. ### The app ```py import writer as wf def handle_multiplication(state): state["n"] = state["a"]*state["b"] wf.init_state({ "counter": 0, "a": 0, "b": 0 }) ``` ### The tests ```py from writer.core import WriterState import main class TestApp: initial_state = main.wf.initial_state artificial_state = WriterState({ "a": 3, "b": 2 }) def test_counter_must_start_from_zero(self): assert self.initial_state["counter"] == 0 def test_handle_multiplication(self): main.handle_multiplication(self.artificial_state) assert self.artificial_state["n"] == 6 ``` # Account management AI Studio has four available user roles based on what parts of the app creation building process those users will be performing. These roles are: 1. **View-only:** Get view-only access to your organization's apps, voices, KGs, etc. (this role can’t make changes across AI Studio). 2. **Individual builder:** Build and maintain your apps, voices, and KGs. 3. **Full access:** Manage deployments across your organization, and create and maintain API keys and Framework apps. 4. **Org admin:** Manage both people and billing. | Feature | View only | Individual builder | Full access | Org admin + Full access | | --------------------------------------------------- | --------- | ------------------ | ----------- | ----------------------- | | View no-code apps | ✅ | ✅ | ✅ | ✅ | | View templates | ✅ | ✅ | ✅ | ✅ | | Create app from template | ❌ | ✅ | ✅ | ✅ | | Duplicate existing no-code app | ❌ | ✅ | ✅ | ✅ | | Create no code app | ❌ | ✅ | ✅ | ✅ | | Delete draft application - created by self | ❌ | ✅ | ✅ | ✅ | | Delete draft application - created by anyone | ❌ | ❌ | ✅ | ✅ | | Modify no code app (draft) - created by self | ❌ | ✅ | ✅ | ✅ | | Modify any no code app | ❌ | ❌ | ✅ | ✅ | | Deploy no code app (embed snippet or to Writer) | ❌ | ❌ | ✅ | ✅ | | Push changes (deploy current draft of existing app) | ❌ | ❌ | ✅ | ✅ | | Regenerate embed token for no code application | ❌ | ❌ | ✅ | ✅ | | API keys (create, revoke, view secret, etc) | ❌ | ❌ | ✅ | ✅ | | Framework apps (create, revoke, view secret, etc) | ❌ | ❌ | ✅ | ✅ | | Open apps in playground & copy playground URL | ✅ | ✅ | ✅ | ✅ | | Enable/disable playground - app created by self | ❌ | ✅ | ✅ | ✅ | | Enable/disable playground - all apps | ❌ | ❌ | ✅ | ✅ | | Create Knowledge Graph | ❌ | ✅ | ✅ | ✅ | | Create Voice | ❌ | ✅ | ✅ | ✅ | | Modify/delete KG - created by self | ❌ | ✅ | ✅ | ✅ | | Modify/delete Voice - created by self | ❌ | ✅ | ✅ | ✅ | | Modify/delete KG - created by anyone | ❌ | ❌ | ✅ | ✅ | | Modify/delete Voice - created by anyone | ❌ | ❌ | ✅ | ✅ | | Invite user to AI Studio | ❌ | ❌ | ❌ | ✅ | | Delete AI Studio user | ❌ | ❌ | ❌ | ✅ | | Change user AI Studio roles | ❌ | ❌ | ❌ | ✅ | | CRUD Billing details | ❌ | ❌ | ❌ | ✅ | | View analytics (when released) | ✅ | ✅ | ✅ | ✅ | # Deployment options Writer’s platform is purpose-built for the enterprise, designed to provide security, compliance, and scalability to support AI deployments at an enterprise scale. We currently offer two platform deployment options: a standard, default option, and dedicated deployment at a fee. Each option comes with tailored features to suit diverse operational needs. In both deployment models, Writer prioritizes data privacy and rigorous protection standards. We offer fully managed application lifecycle management, including monitoring, scaling, and high availability (99.9% uptime). We also adhere to global privacy laws and security standards such as GDPR, CCPA, SOC 2 Type II, PCI, and HIPAA. Across all deployments, customer data is: * Encrypted in transit and at rest * Retained only for the duration necessary to fulfill service requirements * Not used for model training, testing, or improvements To learn more about deploying Writer apps, read our [AI Studio app deployment guide](https://dev.writer.com/framework/cloud-deploy#deploy-to-writer-cloud). Organizations seeking a streamlined, secure, scalable, and cost-effective deployment with minimal management required. ### Features
  • Faster time-to-market with a fully integrated platform and out-of-the-box solution
  • Application-level data isolation and encryption both at rest and in-transit
  • Scalability and cost efficiency, with optimized resource allocation across customers
Organizations with specific data privacy, security, and compliance requirements, or AI workflows involving sensitive data handling or access controls. ### Features
  • Cloud project-level isolation and dedicated API platform with isolated compute, storage, and network
  • Dedicated encryption key for cloud resources with a bring-your-own encryption key option
  • Option to use Private Connect for accessing Writer in the same cloud region as customer workloads
  • Custom domain support available
# Development options Need help building? Writer has an in-house solutions team that can build custom AI apps for you. **[Contact our sales team](https://go.writer.com/contact-sales)** While each development environment is powered by the Writer generative AI platform, they differ in code capabilities, customization options, and use case support. ## Development options comparison You can determine which path is right for you by thinking about what you want to achieve and your technical background: | Platform | Best for | Coding required | Customization scope | | :--------------- | :------------------------------------------------------ | :----------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------- | | No-code builder | Text generation and chat apps | None - Get started fast with prompts and zero coding | Easy, and flexible customization – Chain prompts, set outputs, and adjust variables with ease. | | Writer Framework | Web-based custom apps, ideal for Python developers | Intermediate to Advanced - Customize with Python (flexible for advanced users) | Extensive - Custom components, HTML, iFrame, pages. | | Writer API/SDKs | Embedding AI into existing tech stacks or new languages | Advanced - Full flexibility, integrate in any codebase | Unlimited - Full API access, complete customization for any app | ## Choose your development path No-code tools are ideal for business users who want to quickly build an AI app without writing any lines of code. **With the No-code builder, you can:** * Build AI applications using a visual editor * Build content generation or editing applications * Create internal tools for content teams * Create chat assistants that can also connect to a Knowledge Graph of your data * Integrate any of the Palmyra LLMs ### Quickstart guides * [Build a text generation app](https://dev.writer.com/no-code/building-a-text-generation-app) * [Build a chat app](https://dev.writer.com/no-code/building-a-chat-app) ### Example use cases Using [Knowledge Graph](https://writer.com/product/graph-based-rag/), our graph-based RAG solution, you can build chat assistants to quickly ask questions using your data sources. Automate an entire campaign workflow and generate all launch assets from a single messaging document or GTM presentation. Writer Framework is ideal for Python engineers who want to quickly build an AI-powered app with a visual editor and extensible Python backend. **With Writer Framework, you can:** * Build AI applications using a drag-and-drop visual editor for the frontend, and write backend code in Python * Use external data, like APIs, to bring in information from other sources * Handle complex user input and custom logic * Quickly analyze and visualize data using an LLM * Easily integrate Writer platform capabilities like Knowledge Graphs and tool calling using the AI module **Things to know:** * Install with `pip install writer` (a standard Python package) * UI is saved as JSONL files, so you can version it with the rest of the application. * Two ways to edit the UI: Using your local code editor for instant refreshes or the provided web-based editor * See live updates while editing—no need to hit 'Preview' * **Note**: You will need an API key if you want to use Writer's LLMs and other advanced capabilities in the Writer framework. [Follow the instructions here to sign up for AI Studio and get your Framework API key](https://dev.writer.com/api-guides/quickstart#generate-a-new-api-key). ### Getting started resources * [Writer Framework Quickstart](/framework/quickstart) * [Build a social post generator](/framework/social-post-generator) * [Build a chat assistant](/framework/chat-assistant) ### Example use cases Tool for analyzing financial content based on compliance guidelines. Get the code. AI-powered interactive dashboard for summarizing and visualizing financial data and metrics. Get the code. Check out the app tour on our Engineering blog. Tool to generate promotional content based on uploaded demographic data. Get the code. Uses Palmyra-Med to generate SOAP notes and extract ICD codes from patient-doctor chat. Get the code. Use an embedded no-code chat app to chat with a Knowledge Graph about products. Get the code. Tool for generating formatted product description pages. Get the code. View the full tutorial here. Utility for creating formatted, downloadable release notes from CSVs containing GitLab notes. Get the code. View the full tutorial here. For more sample applications, explore the sample app library. Writer API and SDKs are ideal for engineers who want to embed generative AI into their own stack or access the Writer platform via an API, regardless of the programming language. **With the Writer APIs, you can:** * Use chat and completion APIs to build custom apps, or build custom integrations with your existing tech stack. * Create knowledge graphs using your own files and documents, allowing you to build intelligent Q\&A systems, content recommendation engines, and document retrieval systems that understand context. * Extend the LLM's functionality by enabling it to call your custom functions with tool calling. The model can use these functions to fetch real-time data or perform calculations when generating responses. * Use the RESTful API with any language you prefer, or use our Python SDK or Node SDK. * **Note:** You will need an API key to use Writer APIs/SDKs. [Follow the instructions here to get your API key](https://dev.writer.com/api-guides/quickstart#generate-a-new-api-key). ### Getting started resources * [API Quickstart](/api-guides/quickstart) * [Getting started with Writer SDKs](https://dev.writer.com/api-guides/sdks) * [Python SDK](https://pypi.org/project/writer-sdk/) (`pip install writer-sdk`) * We also have several [Python cookbooks](https://github.com/writer/cookbooks) available to help you get started with common tasks. * [Node SDK](https://www.npmjs.com/package/writer-sdk) (`npm install writer-sdk`) ### Guides * [Text generation](/api-guides/text-generation) * [Chat completion](/api-guides/chat-completion/api-guides/chat-completion) * [Knowledge Graph](/api-guides/knowledge-graph) * [Applications API](/api-guides/applications) * [Tool calling](/api-guides/tool-calling) * [Knowledge Graph chat](/api-guides/kg-chat) ### Example use cases Embed a chat app into an existing tool or service. Add text completion capabilities to an existing tool or service your company already uses. # Introduction Welcome to Writer AI Studio! export const capabilities_2 = " Text input & " export const contextWindow_2 = "32k" export const inputPrice_2 = "5.00" export const outputPrice_2 = "12.00" export const capabilities_1 = " Text input & " export const contextWindow_1 = "32k" export const inputPrice_1 = "5.00" export const outputPrice_1 = "12.00" export const capabilities_0 = " Text input & " export const contextWindow_0 = "128k" export const inputPrice_0 = "5.00" export const outputPrice_0 = "12.00" Writer AI Studio allows you to quickly create and deploy AI apps or integrate generative AI into your existing stack with our platform of **Writer-built LLMs**, **graph-based RAG** (Knowledge Graph), and **AI guardrails**. Build AI digital assistants for various content needs, perform text generation and summarization, conduct data analysis and enhance writing quality, take action through APIs with tool calling, and much more. ## Start building Depending on your use case and technical experience, you can choose from three development options: } > Intuitive tools for building apps that automate repetitive tasks, generate content, or answer questions about company data.
[Build with No-code](/no-code/choosing-an-app-type) →
} > An open-source, Python framework for building feature-rich apps that are fully integrated with the Writer platform.
[Build with Framework](/framework/quickstart) →
} > API endpoints for integrating Writer generative AI technology into apps or services within your own stack.
[Buid with API](/api-guides/quickstart) →
Each option offers different levels of coding and customization, so you can choose what fits your project best. → Explore the [development options guide](/home/development_options) to learn more. ## Explore our models Whatever option you choose, you get access to Writer Palmyra models, offering a range of capabilities for content generation, editing, and automation. Our latest and most advanced model with a large context window
  • {capabilities_0} output
  • {contextWindow_0} context window
  • **Input**: \${inputPrice_0} → **Output**: \${outputPrice_0}
Our finance domain specialized model; first model to pass the CFA exam
  • {capabilities_1} output
  • {contextWindow_1} context window
  • **Input**: \${inputPrice_1} → **Output**: \${outputPrice_1}
Our most sophisticated model for delivering accurate medical analysis
  • {capabilities_2} output
  • {contextWindow_2} context window
  • **Input**: \${inputPrice_2} → **Output**: \${outputPrice_2}
→ Explore all of our [models](/home/models). ## Where to go for help Need help with billing, account management, or enterprise support? Visit our help center. Stuck on something? Have feedback on the docs? Send us an email. Bug or feature request for Writer Feedback? Submit an issue on GitHub. # Language support Writer Palmyra LLM is designed to support over 30 languages, including Arabic, French, Spanish, Hindi, Simplified Chinese, Traditional Chinese, and more. This page provides an overview of our capabilities, performance benchmarks, and prompting examples on how to leverage these features. When it comes to multi-language capabilities, there are two primary categories to consider: generation and translation. Generation typically refers to the ability to understand/create content, answer questions, and converse, all within the same language. Translation typically refers to the ability to transform text to and from English, where either the input or output language is English. On this page, we display two of the many benchmarks we use to evaluate multi-language performance in our Palmyra LLMs. Writer Palmyra has the highest performance of any production LLM in the Holistic Evaluation of Language Models (HELM), an LLM evaluation framework developed by Stanford CRFM to serve as a living benchmark for the community, continuously updated with new scenarios, metrics, and models. While there are limited benchmarks available for evaluating text generation and translation in different languages, we have achieved some of the highest scores in both MMLU and BLEU for other languages. One benchmark that Writer uses to evaluate text generation performance is [MMLU](https://arxiv.org/abs/2009.03300) (Massive Multitask Language Understanding). The [MLMM evaluation](https://arxiv.org/pdf/2307.16039.pdf) covers 57 tasks including elementary mathematics, U.S. history, computer science, law, and more. To attain high accuracy on this test, models must possess extensive world knowledge and problem solving ability. One benchmark that Writer uses to evaluate text translation performance is [BLEU](https://aclanthology.org/P02-1040.pdf) (Bilingual Evaluation Understudy). It's worth noting that [any BLEU score above 60](https://cloud.google.com/translate/automl/docs/evaluate#interpretation) indicates a higher quality translation than a human translation. While Palmyra's core competency lies in the text generation realm, translation use cases are possible. However, it's important to exercise caution in languages where benchmarks are not yet established (we are actively working on establishing these benchmarks). We believe in transparency and advise potential users to be aware of this caveat. Therefore, any outputs or usage of Writer LLM should always be accompanied by the guidance of a human expert. We are continuously evaluating and refining our capabilities, and we are committed to learning with our customers. | Language | MMLU/MLMM | BLEU (source \ English) | | :------------------ | :-------- | :---------------------- | | Arabic | 68.9 | 61.2 | | Bengali | 63.3 | 54.4 | | Bulgarian | 76.3 | 64.2 | | Chinese simplified | 71.7 | 63.8 | | Chinese traditional | 73.7 | 57.0 | | Croatian | 64.9 | 66.4 | | Czech | - | 52.5 | | Danish | 77.7 | 70.5 | | Dutch | 73.6 | 73.9 | | English | 70.2 | - | | Finnish | - | 68.9 | | French | 69.1 | 63.1 | | German | 70.4 | 71.3 | | Greek | - | 60.4 | | Hebrew | - | 67.8 | | Hindi | 77.9 | 68.4 | | Hungarian | 67.7 | 65.3 | | Indonesian | 67.8 | 63.5 | | Italian | 72.5 | 70.9 | | Japanese | 73.5 | 66.8 | | Korean | - | 56.8 | | Lithuanian | - | 59.3 | | Polish | - | 60.6 | | Portuguese | - | 66.2 | | Romanian | 70.9 | 67.6 | | Russian | 75.1 | 65.2 | | Spanish | 72.5 | 79.3 | | Swahili | - | 62.8 | | Swedish | - | 63.2 | | Thai | - | 54.7 | | Turkish | 64.1 | 57.5 | | Ukrainian | 75.2 | 68.0 | | Vietnamese | 72.5 | 60.3 | # Dialect support Writer Palmyra LLM also supports outputting in specific language dialects. The best results come from using a prompt with the following characteristics: 1. The prompt itself is in the desired language and dialect 2. The prompt clearly describes the type of dialect (e.g. "It's essential that you use the Spanish spoken in Spain.") 3. The prompt provides specific examples of the dialect, both vocabulary and grammatical differences The following example, although not in the desired language for simplicity's sake, is an example of an optimal prompt that asks for a translation in Spanish spoken in Spain. > Hello, good afternoon! I need you to help me translate the following text. It's essential that you use the Spanish spoken in Spain. For example, you should use words like "coche" and/or "patata" instead of "carro" and/or "pap." Additionally, you need to pay attention to grammatical differences, such as the use of "voy a por" (Spain) instead of "voy por" (Latin America), or the structure of sentences like "hoy he comido una manzana" instead of of "hoy comí una manzana." I prefer that you use "vosotros" (speak) instead of "ustedes" (speak), unless it's necessary to write very formally. > Here is the text to be translated:\ > \[text you want translated] ## Basic prompt examples ### Translation > Read the content of this source. Provide me with a translation of all its contents in French: [https://writer.com/blog/ai-guardrails/](https://writer.com/blog/ai-guardrails/) ### Text generation > Please write a blog post about the importance of productivity for small businesses in Arabic. ### Native multi-language support > 人工知能の歴史と大規模言語モデルの開発について、短い段落を書いてください。読者はビジネステクノロジーニュースに興味がありますが、技術的なバックグラウンドはありません。技術的な概念を8年生の読解レベルで簡潔に説明してください。 # Mitigating bias ## Introduction In the era of artificial intelligence, ensuring that AI models produce unbiased and fair outputs has become a critical concern. Writer, one of the leading organizations in the field, takes this challenge seriously and employs a range of strategies to detect and mitigate biases in its AI models. In this article, we explore the technical methodologies employed by Writer to grapple with this complex issue. ## Mechanisms and methodologies for detecting and mitigating Bias At the heart of any machine learning model lies the data on which it's trained. Writer meticulously curates its training data, often sourced from a myriad of platforms like websites, books, and Wikipedia. Text preprocessing techniques, such as removing sensitive content or flagging potential hotspots for bias, are employed before the data is used for training. The first layer of oversight comes in the form of human annotators who label the data. These annotators are guided by a stringent set of rules designed to counteract the introduction of bias. For example, they're trained to avoid favoring any political ideology, ethnicity, or other sociocultural factors when annotating the training data. After the initial training, the model is subjected to a rigorous auditing process. This involves running the model through a series of test cases designed to gauge its propensity for biased or unsafe content. These audits are often carried out by both internal teams and third-party organizations to ensure objectivity. The next step involves fine-tuning the model based on the results of the audit. Writer uses reinforcement learning from human feedback (RLHF) or similar techniques to adjust the model's parameters, helping it to make less biased decisions. Fine-tuning focuses on specific aspects like language nuances, sentiment interpretation, and context-aware decision-making. Writer has an active feedback loop with its beta user community. Reports of biased outputs are taken seriously and are used to further fine-tune the model. This makes the model more robust and adaptable to real-world applications. ## Sources of bias in training data Writer models are trained on large datasets that are a snapshot of human culture and thought, collected by our team. While this helps the model to be versatile and knowledgeable, it also brings in the risk of the model inheriting the existing biases in society. Writer mitigates this by adding layers of scrutiny and control, both algorithmic and human, on the data used for training. ## Adapting to different contexts and languages Writer is pioneering research in making its models more context-sensitive. This involves incorporating additional features into the model’s architecture that allows it to understand the specific context in which a text snippet appears, enabling more nuanced responses. Given the global reach of AI, Writer is also working on language-specific versions of its models. These models undergo fine-tuning with data that is representative of the linguistic and cultural idiosyncrasies of specific languages. To further the goal of international adaptability, Writer often employs annotators from various cultural backgrounds. This helps in creating a model that is less likely to favor any particular group. ## Conclusion The challenge of eliminating bias in AI models is a complex and ongoing task. Writer employs a multi-faceted approach, combining data science, human oversight, and cutting-edge machine learning techniques, to tackle this critical issue. While there's always room for improvement, the methodologies adopted serve as a strong framework for mitigating bias in AI. # Models export const capabilities_2 = " Text input & " export const contextWindow_2 = "32k" export const inputPrice_2 = "5.00" export const outputPrice_2 = "12.00" export const capabilities_1 = " Text input & " export const contextWindow_1 = "32k" export const inputPrice_1 = "5.00" export const outputPrice_1 = "12.00" export const capabilities_0 = " Text input & " export const contextWindow_0 = "128k" export const inputPrice_0 = "5.00" export const outputPrice_0 = "12.00" Whether you use no-code, Writer framework, or APIs, you need to choose a model. Here is an overview of the Palmyra models and their capabilities. ## Leading models The input and output price is displayed in 1M tokens. See the [pricing](/home/pricing) page for the full cost. Our latest and most advanced model with a large context window
  • {capabilities_0} output
  • {contextWindow_0} context window
  • **Input**: \${inputPrice_0} → **Output**: \${outputPrice_0}
Our finance domain specialized model; first model to pass the CFA exam
  • {capabilities_1} output
  • {contextWindow_1} context window
  • **Input**: \${inputPrice_1} → **Output**: \${outputPrice_1}
Our most sophisticated model for delivering accurate medical analysis
  • {capabilities_2} output
  • {contextWindow_2} context window
  • **Input**: \${inputPrice_2} → **Output**: \${outputPrice_2}
## Model overview A ✅ indicates the model is available in the product offering, while a ❌ indicates the model is not live for this product offering yet. | Model name | Model ID | No-code | Framework | API | | ---------------------- | ------------------------ | ------- | --------- | --- | | Palmyra X 004 | `palmyra-x-004` | ✅ | ✅ | ✅ | | Palmyra X 003 Instruct | `palmyra-x-003-instruct` | ✅ | ✅ | ✅ | | Palmyra Vision | `palmyra-vision` | ✅ | ❌ | ❌ | | Palmyra Med | `palmyra-med` | ✅ | ✅ | ✅ | | Palmyra Fin 32k | `palmyra-fin-32k` | ✅ | ✅ | ✅ | | Palmyra Creative | `palmyra-creative` | ✅ | ✅ | ✅ | ## Model details `palmyra-x-004` is our newest and most advanced language model with a large context window of up to 128,000 tokens. This model excels in processing and understanding complex tasks, making it ideal for workflow automation, coding tasks, and data analysis. `palmyra-x-003-instruct` is an advanced version of our language model that offers improved performance compared to `palmyra-x-002-instruct`. `palmyra-x-003-instruct` is highly proficient in generating precise and detailed responses. It's particularly useful for applications that require fine-grained language generation. `palmyra-vision` is a language model specifically designed for processing images. It combines the power of natural language processing with computer vision techniques to analyze and generate textual descriptions of images. Palmyra Vision can be used for tasks such as image captioning, visual question answering, and image-to-text generation. `palmyra-med` is a state-of-the-art language model tailored for the healthcare industry, offering a large context window of up to 32,000 tokens. This model is an assistive tool for processing, summarizing and understanding extensive medical texts, making it ideal for comprehensive medical document analysis, patient record summarization, and supporting medical research. This model is not designed to provide medical advice, and must not be used for any diagnostic or therapeutic purposes. It is not to be used in direct patient care. Any output generated by the model must always be reviewed and verified by a qualified and licensed physician, based on their professional judgement, before any use is made of it. `palmyra-fin-32k` is a language model developed specifically for the financial sector, featuring an extensive context window of up to 32,000 tokens. This model is an assistive tool for analyzing and synthesizing long financial documents, including comprehensive market reports, detailed investment analyses, and lengthy regulatory filings. With its ability to grasp complex financial narratives and perform deep contextual analysis, `palmyra-fin-32k` is ideal for applications requiring a thorough understanding of extensive textual information. This model is not designed to assess the suitability of an investment or transaction, and must not be used directly to advise on or recommend any investment or financial transaction. Any output generated by the model must always be reviewed and verified by a qualified and licensed financial advisor, based on their professional judgment, before any use is made of it. `palmyra-creative` is is Writer’s purpose-built language model, engineered to elevate creative thinking and writing across diverse professional contexts. With capabilities that amplify originality and adaptability, it caters to industries and teams where innovation drives success. Available via API, No-code tools, the Writer Framework, and as an NVIDIA NIM microservice, Palmyra Creative is tailored to inspire bold ideas and solutions. ## Deprecation policy ### Timeline for Deprecation * We'll announce the deprecation of a model at least three months in advance. This will give customers time to plan for the migration to the new model. * We'll continue to support deprecated models for a period of time after they're deprecated. This will give customers time to migrate to the new model. * We'll eventually stop supporting deprecated models. The timeline for this will vary depending on the model. We will announce the end of support for a deprecated model at least six months in advance. ### Deprecated models | Model name | Deprecation Date | | ---------------------- | ---------------- | | Palmyra X 002 32k | 2024-09-06 | | Palmyra X 32k Instruct | 2024-09-06 | | Palmyra X 002 Instruct | 2024-09-06 | ### Migration Path * We'll provide customers with a migration path to the new model, including detailed documentation and support to help them migrate their applications. * We'll also offer training and consulting services to help customers transition. # Pricing For any custom pricing, please **[contact our sales team](https://go.writer.com/contact-sales)**. ## Base model The table below outlines our [base model](/home/models#models), which is focused on text input and output. The input and output prices are displayed per 1M tokens unless otherwise specified. | Model name | Model ID | Input / 1M | Output / 1M | | -------------------------------------------------------------------------------------------- | ------------------------ | ---------- | ----------- | | [Palmyra X 004](/home/models#palmyra-x-004) | `palmyra-x-004` | \$5.00 | \$12.00 | | [Palmyra X 003 Instruct](/home/models#palmyra-x-003-instruct) | `palmyra-x-003-instruct` | \$7.50 | \$22.50 | | [Palmyra Med](/home/models#palmyra-med) | `palmyra-med` | \$5.00 | \$12.00 | | [Palmyra Fin 32k](/home/models#palmyra-fin-32k) | `palmyra-fin-32k` | \$5.00 | \$12.00 | | [Palmyra Creative](/home/models#palmyra-creative)                          | `palmyra-creative` | \$5.00 | \$12.00 | ## Palmyra Vision The table below outlines pricing for our [Palmyra Vision](/home/models#palmyra-vision) model which takes in a variety of inputs and produces text as an output. The input and output price is displayed in 1M tokens unless otherwise specified. | Type | Input | Output / 1M | | ----- | -------------- | ----------- | | Image | \$0.015/image | \$22.50 | | Video | \$0.015/second | \$22.50 | | Text | \$7.50/1M | \$22.50 | ## Knowledge Graph The table below outlines pricing for [Knowledge Graph](/api-guides/knowledge-graph#knowledge-graph), our graph-based RAG that grounds generative AI in your company-level context by connecting our platform to your internal data sources. | Capability | Cost | | ----------------------- | ----------------------------- | | Knowledge Graph hosting | \$0.085/gb of storage per day | | Data extraction | \$0.00015/page | | File parsing (OCR) | \$0.055/page | | Web Scraper | \$0.12/page | Data connectors are part of our Enterprise plan, to learn more please **[contact our sales team](https://go.writer.com/contact-sales)** ## Tools API The table below outlines pricing for our [Tools API](/api-guides/api-reference/tool-api/), which provides utilities for pre-processing and comprehending text. | Tool | Cost | | ------------------------------------------------------------------------------------------ | ------------------------------------------- | | [Context-aware text splitting](/api-guides/api-reference/tool-api/context-aware-splitting) | \$0.04 per 1M words input/output | | [Medical comprehend](/api-guides/api-reference/tool-api/comprehend-medical) | \$0.02 per 1M words input (no output costs) | | [PDF parser](/api-guides/api-reference/tool-api/pdf-parser) | \$0.055/page | ## Deprecated models The table below are our deprecated models. To learn more see the [deprecation policy](/home/models#deprecation-policy). | Model name | Deprecation Date | Input / 1M | Output / 1M | | ---------------------- | ---------------- | ---------- | ----------- | | Palmyra X 002 32k | 2024-09-06 | \$1.00 | \$2.00 | | Palmyra X 32k Instruct | 2024-09-06 | \$1.00 | \$2.00 | | Palmyra X 002 Instruct | 2024-09-06 | \$1.00 | \$2.00 | # Prompt security Security measures against prompt injections, jailbreak attacks, and secrets leakage ## Introduction As AI systems like Writer models become increasingly integrated into a wide range of applications, the importance of securing them against various types of attacks becomes paramount. In this article, we delve into the technical details of how Writer safeguards its AI models against prompt injections, jailbreak attempts, and the leakage of sensitive information. ## Measures against prompt injections One of the first lines of defense is input sanitization, where incoming prompts are screened for potentially malicious code or harmful strings. Sophisticated Natural Language Processing (NLP) techniques, combined with standard cybersecurity measures, are used to detect and remove or neutralize suspicious inputs. To prevent brute-force attacks and other malicious activities, Writer often implements rate limiting on the API requests. This ensures that a user can't overload the system with a large number of potentially harmful prompts in a short amount of time. Role-cased access control (RBAC) to more advanced or potentially risky functionalities is restricted based on roles. This prevents unauthorized users from injecting malicious prompts that could potentially exploit these functionalities. ## Safeguards against jailbreak attacks The runtime environment in which the AI models operate is isolated from the rest of the system. This isolation is often achieved using containerization technologies like Docker, which limit the resources and system calls available to the running model. Advanced anomaly detection systems are put in place to monitor the behavior of the AI models in real-time. Any unusual patterns or inconsistencies could trigger alerts, initiating immediate investigation and potential shutdown of the affected instance. Before deployment, the codebase undergoes rigorous reviews and static analysis to ensure that there are no vulnerabilities that could be exploited to "jailbreak" the AI system. ## Measures against secrets leakage All sensitive information, including security keys and credentials, is encrypted using state-of-the-art encryption algorithms. These encrypted secrets are stored in secure vaults that are accessible only to authorized personnel. A Zero trust architecture is employed, which means that every request or interaction with the AI system is treated as potentially malicious until proven otherwise. This adds an extra layer of security against unauthorized access and data leakage. ## Conclusion Security is a multi-faceted challenge that requires a holistic approach. Writer employs a combination of advanced technologies and best practices to safeguard its AI models against prompt injections, jailbreak attacks, and the leakage of sensitive information. While no system can be 100% secure, these measures provide a robust framework for identifying, mitigating, and responding to various security threats. # Prompting strategies export const PromptComponent = ({prompt}) =>

{prompt}

; This guide covers the [fundamentals](/home/prompting#fundamentals) behind prompting strategies and discusses some [advanced techniques](/home/prompting#advanced-techniques). ## Fundamentals Be direct and to the point:

Include your audience in your prompt:

Format your prompt with *### Instruction ###*, followed by *### Question \###*:

### Instruction ###
Write a poem in the style of Robert Frost.

### Question ###
What's the poem about?

Break down complicated tasks into multiple prompts:

Use directives like "do" instead of "don't":

Use phrases like *You MUST*:

Use phrases like *You'll be penalized*:

Request a simple response:

Make sure that your answer is unbiased and doesn’t rely on stereotype:

To write an essay, text, paragraph, article, or any detailed text:

Improve the tone of the text to make the writing style more professional:

I'll give you the beginning of a story. Finish it and keep the flow consistent:

Use the same language as the following paragraph to explain the importance of exercise:

Clearly state the requirements that the model must follow to produce content, in the form of keywords or instructions.

## Advanced techniques Use example-driven (few-shot) prompting:

Provide a list of idioms in English, along with their meanings and example sentences.
Here is an example for the output:

An idiom: 'Break a leg'
Meaning: Good luck
Example sentence: 'I'm sure you'll do great in your interview. Break a leg!'

Combine chain-of-thought (CoT) prompts with few-shot prompts:

System instruction: Create a product detail page for a fictional innovative smartphone by a retailer known as "TechTrend Electronics."
Prompt 1: Start by describing the unique features of the smartphone, such as its solar-powered battery, triple-lens camera system, and foldable screen technology.
Prompt 2: Next, outline the benefits of these features for users, like extended battery life, exceptional photo quality, and enhanced device portability.
Prompt 3: Conclude with crafting compelling product descriptions and a call-to-action that entices customers to make a purchase during the upcoming holiday sale.

Use delimiters to structure text:

Summarize a series of healthcare claims documents for a fictional healthcare company, 'HealthFirst Solutions', using the following delimiter `\n` to separate different sections of the summary:
Claim Number: 123456789 `\n`
Date of Service: January 1, 2024 `\n`
Diagnosis: Acute sinusitis `\n`
Total Claimed: \$300 `\n`
Status: Pending review `\n`

# Research papers Below are research papers published by the Writer AI, NLP, and Data Science team. Link to paper

In this paper, we introduce the IFS, a metric for instruction following. The metric detects language models' ability to follow instructions. First, IFS can distinguish between base and instruct models. We benchmark public bases and models, showing they're well-formatted responses to partial and full sentences are effective. The metric can be used as a measure between model classes.

We compute IFS for Supervised early stopping. Follow instructions early and finetune later. As an example, we show model predictions are objective. We show that semantic changes can be caused by auxiliary metric ObjecQA. When IFS decomposes, it steepens. IFS and semantic factors start a controllable instruct trend. Tuning and querying opens minimal instruct interfaces Foundation models are short-lived.

Link to paper

Palmyra-20b and Palmyra-40b are two cutting-edge large language models (LLMs) that were fine-tuned and evaluated for medical language understanding tasks. By applying instruction-based fine-tuning on a custom-curated medical dataset of 200,000 examples, we create novel, fine-tuned models, Palmyra-Med-20b and Palmyra-Med-40b. Performance is then measured across multiple medical knowledge datasets, including PubMedQA and MedQA.

Our fine-tuned models outperform both their base counterparts and other LLMs pre-trained on domain-specific knowledge. This research demonstrates the effectiveness of instruction-based fine-tuning in enhancing LLMs performance in the medical domain.

Link to paper

Grammatical Error Correction (GEC) is the task of automatically detecting and correcting errors in text. The task not only includes the correction of grammatical errors, such as missing prepositions and mismatched subject-verb agreement, but also orthographic and semantic errors, such as misspellings and word choice errors respectively. The field has seen significant progress in the last decade, motivated in part by a series of five shared tasks, which drove the development of rule-based methods, statistical classifiers, statistical machine translation, and finally neural machine translation systems which represent the current dominant state of the art.

In this survey paper, we condense the field into a single article and first outline some of the linguistic challenges of the task, introduce the most popular datasets that are available to researchers (for both English and other languages), and summarise the various methods and techniques that have been developed with a particular focus on artificial error generation. We hope that this survey will serve as comprehensive resource for researchers who are new to the field or who want to be kept apprised of recent developments.

Link to paper

Open-domain question answering (QA) has recently made significant progress, with generative models like Transformers demonstrating impressive performance. However, these models are computationally expensive to train and query, limiting their practical application. In this whitepaper, we introduce a novel approach to open-domain QA that combines the strengths of retrieval and generative models, aiming to achieve more efficient and accurate question answering.

Our approach, termed Fusion-in-Decoder, retrieves informative passages and leverages them with a sequence-to-sequence model to generate answers. This method demonstrates state-of-the-art results on benchmarks like Natural Questions and TriviaQA, and offers a highly scalable framework for aggregating and combining information from multiple passages.

Link to paper

For decades, human-computer interaction has fundamentally been manual. Even today, almost all productive work done on the computer necessitates human input at every step. Autonomous virtual agents represent an exciting step in automating many of these menial tasks. Virtual agents would empower users with limited technical proficiency to harness the full possibilities of computer systems. They could also enable the efficient streamlining of numerous computer tasks, ranging from calendar management to complex travel bookings, with minimal human intervention.

In this paper, we introduce OmniACT, the first-of-a-kind dataset and benchmark for assessing an agent's capability to generate executable programs to accomplish computer tasks. Our scope extends beyond traditional web automation, covering a diverse range of desktop applications. The dataset consists of fundamental tasks such as "Play the next song", as well as longer horizon tasks such as "Send an email to John Doe mentioning the time and place to meet". Specifically, given a pair of screen image and a visually-grounded natural language task, the goal is to generate a script capable of fully executing the task.

We run several strong baseline language model agents on our benchmark. The strongest baseline, GPT-4, performs the best on our benchmark However, its performance level still reaches only 15% of the human proficiency in generating executable scripts capable of completing the task, demonstrating the challenge of our task for conventional web agents.Our benchmark provides a platform to measure and evaluate the progress of language model agents in automating computer tasks and motivates future work towards building multimodal models that bridge large language models and the visual grounding of computer screens.

Link to paper

This research paper presents a comprehensive analysis of integrating advanced language models with search and retrieval systems in the fields of information retrieval and natural language processing. The objective is to evaluate and compare various state-of-the-art methods based on their performance in terms of accuracy and efficiency.

The analysis explores different combinations of technologies, including Azure Cognitive Search Retriever with GPT-4, Pinecone's Canopy framework, Langchain with Pinecone and different language models (OpenAI, Cohere), LlamaIndex with Weaviate Vector Store's hybrid search, Google's RAG implementation on Cloud Vertex AI Search, Amazon SageMaker's RAG, and a novel approach called KG-FID Retrieval.

The motivation for this analysis arises from the increasing demand for robust and responsive question-answering systems in various domains. The RobustQA metric is used to evaluate the performance of these systems under diverse paraphrasing of questions. The report aims to provide insights into the strengths and weaknesses of each method, facilitating informed decisions in the deployment and development of AI-driven search and retrieval systems.

Link to paper

In this paper, we introduce Writing in the Margins (WiM), a new inference pattern for Large Language Models designed to optimize the handling of long input sequences in retrieval-oriented tasks. This approach leverages the chunked prefill of the key-value cache to perform segment-wise inference, which enables efficient processing of extensive contexts along with the generation and classification of intermediate information ("margins") that guide the model towards specific tasks.

This method increases computational overhead marginally while significantly enhancing the performance of off-the-shelf models without the need for fine-tuning. Specifically, we observe that WiM provides an average enhancement of 7.5% in accuracy for reasoning skills (HotpotQA, MultiHop-RAG) and more than a 30.0% increase in the F1-score for aggregation tasks (CWE).

Additionally, we show how the proposed pattern fits into an interactive retrieval design that provides end-users with ongoing updates about the progress of context processing, and pinpoints the integration of relevant information into the final response. We release our implementation of WiM using Hugging Face Transformers library at [https://github.com/writer/writing-in-the-margins](https://github.com/writer/writing-in-the-margins).

# Transparency Ensuring transparency and accountability in AI systems ## Introduction The complexity of deep learning models like Palmyra-X makes them remarkably powerful but also increasingly opaque. As the adoption of these AI technologies expands, the necessity for transparency and accountability becomes critical. Writer addresses these concerns by employing a multi-tiered approach that leverages cutting-edge algorithms and technologies. This article provides a detailed technical perspective on these strategies. ## Tools and algorithms for insights into decision-making In transformer-based models like Palmyra, attention mechanisms play a crucial role in determining output. By visualizing the weights in the multi-headed attention layers, one can gain insights into which input tokens significantly influence the output. Algorithms like layer-wise relevance propagation can be employed to decompose these attention scores. SHAP (SHapley Additive exPlanations) values are derived from cooperative game theory and offer a unified measure of feature importance. By computing SHAP values for each feature in the input data, one can quantify how much each feature contributes to a particular decision, thus offering a granular view of the model's inner workings. A robust logging system captures not just input-output pairs but also intermediate representations, attention maps and activation functions. Tools like TensorBoard with custom dashboards are used for real-time monitoring and auditing at testing stage. Traditional A/B testing is enhanced with Bayesian statistical methods to rigorously compare the performance and decision-making processes of different model versions. This provides confidence intervals and posterior distributions that offer more nuanced insights than point estimates. ## Addressing the 'opaque' nature of AI: explainability and transparency Local Interpretable Model-agnostic Explanations (LIME) is used to create surrogate models that approximate the behavior of the complex model in the vicinity of the instance being explained. By perturbing the input and observing the output, LIME fits a simple model that is easier to interpret, thus shedding light on the original model's decision-making process. Counterfactual explanations provide "what-if" scenarios that help understand how a different input could lead to a different output. We use DiCE Algorithm (Diverse Counterfactual Explanations) to generate these scenarios. Releasing the models under an open-source Apache 2.0 license allows for community-based auditing. Skilled developers and researchers can scrutinize the codebase, algorithms, and even contribute to enhancing transparency features. To safeguard user data while maintaining transparency, differential privacy algorithms like Laplace noise addition or Differential Privacy SGD are implemented. This allows the model to be queried for insights without revealing any individual data points. ## Compliance and regulations For regulatory compliance, techniques such as Automatic Fairness Verification and Fairness-aware Learning are integrated into the model training pipeline. These ensure that the model meets standards like GDPR, which mandates the right to explanation for automated decisions. ## Conclusion Transparency and accountability in AI models are complex challenges that require a multi-layered, algorithmically robust approach. By employing advanced techniques like SHAP, LIME‌ and Bayesian A/B testing, Writer aims to open up the "black box" of its AI models. While full transparency remains a moving target, these technologies and methodologies provide a comprehensive framework for making significant strides in understanding and auditing AI systems. # Building a chat app ### Writing a welcome message A welcome message is the first message your users will see. Greet them, tell them how to use the app, and include any special instructions or anything else they should know. ![welcome-message](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/welcome-message.png) ### Creating an avatar Choose an avatar for your app. This can be your company logo or any other image you prefer. ![creating-avatar](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/creating-avatar.png) Currently only .svg files are supported. ### Choosing a mode To choose the right mode for your app you need to decide how your users will interact with it. If you want them to ask general questions, choose the **General chat mode**. If you want them to ask specific questions about your company’s data, choose the **Knowledge Graph mode**. [Knowledge Graph](https://support.writer.com/category/241-knowledge-graph) lets you ask questions about your company's data and get reliable, accurate answers. It uses multiple internal sources, making it easy to find information even if you're not sure where it's stored. Read more [here](https://support.writer.com/article/244-how-to-use-knowledge-graph). Here is a detailed breakdown: #### General chat mode * Use this mode to ideate or create content #### Knowledge Graph mode * Use this mode to get answers from your company’s data * Before you can use Knowledge Graph mode, you need to set up a [Knowledge Graph](https://support.writer.com/article/244-how-to-use-knowledge-graph) with all the relevant files for your use case. You can do this from the side navigation bar. You won’t be able to build an app with a Knowledge Graph if no Knowledge Graph exists. Read how to set it up [here](https://support.writer.com/article/242-how-to-create-and-manage-a-knowledge-graph). To build an app, you need to select at least one mode. If you’re not sure which one to choose, select the General chat app mode. ![choosing-mode.png](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/choosing-mode.png) ### Providing instructions Instructions are the system messages that reach the LLM and can be used to provide context or structure to how your chat app responds to requests. For example, you can use instructions to: 1. Request answers in a specific language by providing examples. 2. Patch stale data in your Knowledge Graph that’s hard to retrieve by providing additional information. 3. Provide context about the users and how to address them. 4. Set limits on the topics that can be answered. Your instructions will tell the LLM what answers to generate and how to format them. [Knowledge Graph](https://support.writer.com/category/241-knowledge-graph) helps you find information from multiple sources across your organization. Ask questions and get answers based on your data. Think of it as having a knowledgeable virtual assistant that provides insights and helps you respond to your customers. Read more [here](https://support.writer.com/article/244-how-to-use-knowledge-graph). # Building a text generation app Check out our [prompting guide](/home/prompting) for strategies on how to write effective prompts. Every text generation app consists of [adding inputs](/no-code/building-a-text-generation-app#adding-an-input), [writing prompts](/no-code/building-a-text-generation-app#writing-prompts), and [formatting outputs](/no-code/building-a-text-generation-app#formating-the-output). ![text-generation-ap](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/text-generation-app.png) An input is content information for your app. It generates the desired output—the content your app will produce. For example, if you're building a blog post app, the input would include the blog post topic, outline, keywords, reference materials, research sources, and existing knowledge about the topic. Types of inputs: 1. Text input 2. Dropdown 3. File upload 4. Media upload ![adding-input](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/adding-input.png) Once you’ve decided what type of input you want to add, you’ll need to: Give your input a name that clearly communicates its purpose to users. This will help them understand what information they need to provide. If it’s required, users must enter information in that field in order to use the app. If it’s optional, they can skip it. Provide placeholder text or descriptions to clarify what information is required. This ensures that users fully understand what they need to enter. If your app expects a specific type of an .XLSX file, restrict other file types from being allowed. This will guide users to use the app as intended. #### What’s a prompt? A prompt is a message that you send to the model. It can be a single prompt consisting of a few lines, or a complex prompt consisting of multiple steps—each with its own prompt that relies on multiple inputs. You can even use prompting techniques such as chain-of-thought (CoT) prompting, where one prompt output becomes an input to the next prompt, and so on—creating a chain of thought. #### How to use prompts effectively You can ask the model to perform various tasks, but it's best to break down your prompts into smaller, more specific "asks." This is especially important for complex, multi-step apps. Otherwise, you risk overwhelming the model by asking it to perform too many tasks at once. For example, instead of asking it to create an entire blog post at once, you might write one prompt asking it to create a title, another to create an opening paragraph, and so on. Follow these [strategies](/home/prompting) to write good prompts. ![post-version-a](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/post-version-a.png) #### Renaming prompts You can rename prompts from generic names like "Prompt 1" and "Prompt 2" to any custom name. This will help you to: 1. Stay organized and keep track of your prompts 2. Know exactly what each prompt accomplishes 3. Write your prompts 4. Format your output ![prompts.png](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/prompts.png) ### Referencing inputs To reference your input, `@` it within the prompt. For example, if you're writing a prompt to generate social posts based on a blog post, do the following: 1. **Input:** `Blog post` 2. **Prompt 1:** instructions for creating a social post In your prompt, reference `@Blog post` so that Writer understands that the social post should be based on the blog input. ![inputs](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/inputs.png) If you have more than one input, you can `@` each one within a single prompt. ### Referencing prompts You can reference other prompts within a prompt by using the `@` symbol, a feature known as prompt chaining. For example, if you have a long blog post, you might want to summarize it in your first prompt, and use that summary to generate a social media post in your second prompt: `Long blog` Instructions for generating a summary of the `@Long blog` post. Instructions for generating a social media post based on `Prompt 1`. In the second prompt, you’d reference `@Prompt 1` to indicate that you want the social media post to be based on the blog summary. ![referencing-prompt](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/referencing-prompts.png) ### Formatting the output Next, format your output—the final assembled app. To do this, take all your prompt outputs (if you have more than one), format their style, add any static text such as a disclaimer, and combine them together. You can use Markdown or HTML to specify how you’d like your output to be formatted. For example, you can use the following formatting: Use `

`, `

`, `

`, `

`, and `

`, `

` to denote heading text.
Use `` and `` to denote bolded text.
![output-formatting](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/output-formatting.jpeg) Here's the final app that your users will see. ![final-app](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/final-app.png) The simplest app will have only one prompt. A more complex app might have multiple section headers, each with its own prompt output. Before adding even more prompts to your app, ask yourself: 1. Does the user want this content? 2. Do I need these inputs to generate this output? You don't have to include every prompt in your output formatting. For example, a prompt might be used only to summarize and then to create a LinkedIn post (based on the summary). If you don't want to include the summary in your output, you don't have to. Because you don’t need to reference every prompt in your final output, you can leave interim prompts in your apps for A/B testing or to get feedback. Combined with the `Rerun` option, this is a great way to efficiently iterate on your apps. # Choosing an app type Select the app type that best fits your use case: 1. **Chat app:** Best for conversations, answering questions, and research. 2. **Text generation app:** Best for long- or short-form structured content, such as blog posts, FAQs, press releases, and newsletters. Define the structure of the app by outlining the inputs (the information you provide) and the outputs (the content you receive). Here are some input and output examples: ## Chat app **Inputs:** * User queries about specific topics (e.g., "What are your store hours?") * User requests for assistance (e.g., "Help me track my order.") **Outputs:** * Direct answers to user queries (e.g., "Our store hours are 9 AM to 9 PM, Monday through Friday.") * Assistance responses and guidance (e.g., "I can help you track your order. Please provide your order ID.") ## Text generation app **Inputs:** * Content briefs or outlines that specify the topic you want to write about, the SEO keywords and a CTA you want to include. **Outputs:** * Completed text that includes SEO keywords and the specific CTA There are currently only two types of apps, but we're working on adding more. If you have another app type in mind, please [let us know](https://support.writer.com/)! # Deploying an app Only full-access users can deploy apps. Depending on your organization's status, you'll have up to four deployment options. ![deploying-app](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/deploying-app.png) Playground is the easiest way to share and test your app. Just go to **Deploy** and turn on the Playground toggle. You can either copy the Playground URL to share with others, or open the app in **Playground** in a new tab. ![playground](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/playground.png) The **Open** button will take you directly to this app through the Playground, while the **Copy link** option will generate a shareable link that you can send to any tester or end user to get feedback on your app. Links to the Playground view of an app don't require authentication. This can be useful for providing external users access to test an app (they won't be able to navigate elsewhere within your AI Studio). There are two embed styles available: full page and sidebar. After enabling embed, select your preferred style, and you'll see a block of code that contains the snippet you need to include wherever you want, such as on your own website or portal. This is an HTML iframe, and you'll probably need to share this snippet with your engineering team to embed this app. You can disable the embed snippet at any time in AI Studio, and any changes you push will immediately update the embeds. ![embed-app](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/embed-app.png) If your organization uses chat apps with General chat mode or Knowledge Graph mode with AI Studio graphs, you’ll see a Deploy to Slack option. This allows you to enable the app for use within Slack. Once deployed, the app will be available in Slack, and you’ll be able to access it directly from within your workspace. Once the app is Slack-enabled, you can start interacting with it by clicking the Writer button in Slack and selecting "Connect to Writer." ![playground](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/slack-1.png) Next, you’ll need to go through the OAuth flow. ![playground](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/slack-2.png) When you return to Slack, the app will be available for use. You’ll see that the Q\&A functionality is now integrated within Slack. ![playground](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/slack-3.png) When using the app in Slack, you’ll have two interaction modes: * **General mode:** If the app uses General mode and doesn’t include a Knowledge Graph mode, simply select it and start chatting. * **Knowledge Graph mode:** If the app includes a Knowledge Graph, select the interaction mode—either Knowledge Graph mode or General mode. * In General mode, it’ll function like a non-graph-enabled app. * In Knowledge Graph mode, you'll be prompted to connect the relevant graphs. Once connected, you can start chatting with the app. ![playground](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/slack-4.png) If the app is built with specific data constraints (e.g., a sales Q\&A app linked only to Salesforce data), you’ll select the Knowledge Graph, and the setup will be complete without needing to choose different graphs. This deployment mode leverages the configurations set in the app builder, ensuring the app behaves as expected in Slack **Note:** Deployed apps in Slack will charge token usage. Please refer to our [pricing page](/home/pricing) for more details. You’ll have access to this deployment option if your organization uses chat apps with General chat mode or Knowledge Graph mode with AI Studio graphs. If your organization uses the Writer app in addition to AI Studio, you'll see a **Deploy to Writer** option. This allows you to select specific teams in Writer where you want to deploy the app. Once deployed, the app will be available to those teams within the app library. You can choose to deploy the app to all teams or select specific teams. This deployment mode also uses the information provided in the app configuration tab. This information is displayed to users to provide helpful guidance on how to use the application and how to tag it for easy retrieval. You’ll have access to this option if you have access to the Writer app. ![deploy](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/deploy.png) # Editing an app To edit your deployed app: * Click **Unlock to edit** to change either the app configuration or the app itself. * Click **Push all changes** to update the deployed instances of your app. To edit the app’s name or metadata: * Click the down arrow and select **Push configuration changes only** or **Push guide changes only**. ![editing-app](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/editing-app.png) # Introduction No-code tools let you build apps **without writing any code** to compress your workflows—automate repetitive tasks, create branded content, and develop knowledge assistants based on your company's data. Simply define the required inputs, the instructions for the app to follow, and the desired output structure. Once you've built the app, deploy it and edit it at any time. ![no-code-tools](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/no-code-tools.png) You can build no-code apps for: 1. **Recurring** **tasks** that require repetitive processing, such as creating recurring content. 2. **Time-intensive tasks** that require extensive back-and-forth communication between contributors to complete. 3. **Structured** **tasks** that require the same information to be requested and always produce the same result. 4. **Voice-specific** **tasks** that require a particular tone, style, or format. Explore the pre-built, no-code apps in [Templates](https://writer.com/templates/). When you find one you like, click "Try in builder" to copy and test it. If it meets your needs, you can deploy it immediately and edit it after deployment. # Use cases export const PromptComponent = ({prompt}) =>

{prompt}

; ## Text generation apps ### Social package When you publish a great blog post and want to share it across multiple social media platforms, creating unique LinkedIn, X, and Instagram posts can feel overwhelming. With no-code, you can create a "social package" app that generates a customized LinkedIn, X, and Instagram post with just one click, in the voice, tone, and structure that you want. Here's how it works: Use a *File upload* or a *Text input* where you can copy and paste the blog content directly. Create separate prompts for each social media platform with its own parameters and requirements. For example, you may want your LinkedIn posts to sound more professional than your Twitter or Instagram posts. Consider including specific instructions for each platform, such as the desired length, tone, and any required hashtags. Take a sample blog post and generate social media posts using your prompts. You may need to tweak your prompts to get the results you want. Consider including instructions for voice, tone, length, and even specific emojis you want to include. In *Output formatting*, use `@` to mention any prompts you want the user to see. Organize this section so that it's clear which post is intended for LinkedIn, Instagram, or other platforms. Once you're happy with the functionality and look of your app, deploy it in Writer so that everyone on your team can benefit from it. ![social-package](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/social-package.png) Check out our [video](https://www.loom.com/share/41921ee84a474ccfb22ada20e44ae8a6?sid=77e75612-4d92-4134-b5ab-3dc7e5653f3b) on how to create a blog post. ### Blog post Writing a blog post from scratch involves time-consuming tasks like research, keyword optimization, outlining, writing, and editing. No-code can help speed up this process. There are several ways to build an app to generate a blog post. For example, you can have the app generate a blog outline and then write the blog based on that outline, or you can write the outline yourself and use it in the app to generate the blog post. Here is how to do it step by step:
  • File upload input: Upload articles related to your blog topic.
  • Text input 1: Write a blog outline.
  • Text input 2: Include SEO keywords to be mentioned in the blog.
  • Break the prompts into smaller, manageable chunks:
  • Generate a blog title (include instructions for generating multiple options).
  • Generate the blog body.
  • Generate the blog conclusion.
  • Review the generated content and make necessary adjustments.
  • Control the complexity of the language used.
  • Adjust the length of the conclusion.
  • Ensure the inclusion of SEO keywords by providing explicit instructions.
  • Use `@mentions` to indicate different sections (e.g., body, conclusion).
  • Add text to clarify which section is which.
  • When you're satisfied with the results, deploy your app to Writer for others to use.
  • ![blog-post](https://mintlify.s3.us-west-1.amazonaws.com/writer/images/no-code/blog-post.png)