diff --git a/docs/framework/ai-module.mdx b/docs/framework/ai-module.mdx index 4754f5725..aab86f566 100644 --- a/docs/framework/ai-module.mdx +++ b/docs/framework/ai-module.mdx @@ -138,7 +138,7 @@ graph.add_file(file) # Remove the file from the graph graph.remove_file(file) -```` +``` #### Applying Graphs to Conversation completion @@ -228,10 +228,10 @@ response = conversation.complete(tools=tool, max_tool_depth=7) 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 FunctionTool, retrieve_graph +from writer.ai import create_function_tool, retrieve_graph # Define a function tool -tool1 = FunctionTool( +tool1 = create_function_tool( name="get_data", callable=lambda x: f"Data for {x}", parameters={"x": {"type": "string", "description": "Input value"}} @@ -245,7 +245,9 @@ response = conversation.complete(tools=[tool1, graph]) ``` ## Text generation without a conversation state -These `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. + +### 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 @@ -260,4 +262,81 @@ for text_chunk in stream_complete("Explore the benefits of AI.", config={'temper ``` +### 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) +``` + diff --git a/docs/framework/public/components/progressbar.png b/docs/framework/public/components/progressbar.png new file mode 100644 index 000000000..872528352 Binary files /dev/null and b/docs/framework/public/components/progressbar.png differ diff --git a/docs/framework/seo.mdx b/docs/framework/seo.mdx index 01af89993..085b6ed49 100644 --- a/docs/framework/seo.mdx +++ b/docs/framework/seo.mdx @@ -1,5 +1,5 @@ --- -title: "SEO" +title: "Seo and social sharing" mode: "wide" --- @@ -41,6 +41,7 @@ writer.serve.configure_webpage_metadata( You can also use a function to generate the meta tags dynamically. +*./server_setup.py* ```python def _meta(): last_news = db.get_last_news() @@ -57,6 +58,7 @@ writer.serve.configure_webpage_metadata(meta=_meta) 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= { @@ -66,9 +68,11 @@ writer.serve.configure_webpage_metadata( "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() diff --git a/docs/mint.json b/docs/mint.json index 767ba68eb..246cf5839 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -32,23 +32,20 @@ "framework/introduction", "framework/quickstart", "framework/ai-module", - "framework/cloud-deploy", - "framework/sample-apps" + "framework/sample-apps", + "framework/component-list-link" ] }, { "group": "Guides", "pages": [ "framework/application-state", - "framework/event-handlers", + "framework/backend-initiated-actions", "framework/builder-basics", + "framework/event-handlers", "framework/handling-inputs", - "framework/dataframe", - "framework/backend-driven-ui", - "framework/stylesheets", - "framework/frontend-scripts", - "framework/custom-components", - "framework/authentication" + "framework/dataframe", + "framework/repeater" ] }, { @@ -64,19 +61,23 @@ "group": "Deployment", "pages": [ "framework/cloud-deploy", - "framework/deploy-with-docker", - "framework/testing" + "framework/deploy-with-docker" ] }, { "group": "Advanced", "pages": [ - "framework/repeater", - "framework/backend-initiated-actions", + "framework/authentication", + "framework/backend-driven-ui", + "framework/custom-components", + "framework/custom-server", + "framework/frontend-scripts", "framework/page-routes", "framework/sessions", - "framework/custom-server", - "framework/state-schema" + "framework/state-schema", + "framework/stylesheets", + "framework/testing", + "framework/seo" ] }, { diff --git a/pyproject.toml b/pyproject.toml index 7ec18f681..ab7e2d602 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "writer" -version = "0.8.3rc1" +version = "0.8.3rc2" description = "An open-source, Python framework for building feature-rich apps that are fully integrated with the Writer platform." authors = ["Writer, Inc."] readme = "README.md" diff --git a/src/ui/src/components/core/base/BaseInputWrapper.vue b/src/ui/src/components/core/base/BaseInputWrapper.vue index 1cb3b391b..ed3b64273 100644 --- a/src/ui/src/components/core/base/BaseInputWrapper.vue +++ b/src/ui/src/components/core/base/BaseInputWrapper.vue @@ -4,16 +4,16 @@ class="BaseInputWrapper" :class="{ horizontal: isHorizontal }" > - + diff --git a/src/ui/src/components/core/input/CoreSliderInput.vue b/src/ui/src/components/core/input/CoreSliderInput.vue index b2d8b1971..4d2bf49e1 100644 --- a/src/ui/src/components/core/input/CoreSliderInput.vue +++ b/src/ui/src/components/core/input/CoreSliderInput.vue @@ -103,6 +103,7 @@ const { formValue, handleInput } = useFormValueBroker( wf, instancePath, rootInstance, + 50, ); diff --git a/src/ui/src/components/core/input/CoreSliderRangeInput.vue b/src/ui/src/components/core/input/CoreSliderRangeInput.vue index 3ebd4db60..332fe0588 100644 --- a/src/ui/src/components/core/input/CoreSliderRangeInput.vue +++ b/src/ui/src/components/core/input/CoreSliderRangeInput.vue @@ -6,7 +6,7 @@ > ( wf, instancePath, rootInstance, + [20, 50], ); diff --git a/src/ui/src/components/workflows/WorkflowsWorkflow.vue b/src/ui/src/components/workflows/WorkflowsWorkflow.vue index 6d2cb2166..8df276866 100644 --- a/src/ui/src/components/workflows/WorkflowsWorkflow.vue +++ b/src/ui/src/components/workflows/WorkflowsWorkflow.vue @@ -382,8 +382,8 @@ function moveNode(ev: MouseEvent) { const component = wf.getComponentById(nodeId); const { x, y } = getAdjustedCoordinates(ev); - const newX = x - offset.x; - const newY = y - offset.y; + const newX = Math.floor(x - offset.x); + const newY = Math.floor(y - offset.y); if (component.x == newX && component.y == newY) return; diff --git a/src/ui/src/composables/useFieldValue.ts b/src/ui/src/composables/useFieldValue.ts new file mode 100644 index 000000000..595f1f93a --- /dev/null +++ b/src/ui/src/composables/useFieldValue.ts @@ -0,0 +1,10 @@ +import { ComputedRef, computed } from "vue"; + +type Fields = Record>; + +export function useFieldValueAsYesNo( + fields: Fields, + key: string, +): ComputedRef { + return computed(() => fields[key].value === "yes"); +} diff --git a/src/ui/src/composables/useFormatter.ts b/src/ui/src/composables/useFormatter.ts new file mode 100644 index 000000000..da47c5912 --- /dev/null +++ b/src/ui/src/composables/useFormatter.ts @@ -0,0 +1,18 @@ +import { computed, ComputedRef } from "vue"; + +export function usePercentageFormatter( + number: ComputedRef, + options: Pick< + Intl.NumberFormatOptions, + "minimumFractionDigits" | "maximumFractionDigits" + > = { + minimumFractionDigits: 0, + maximumFractionDigits: 1, + }, +) { + const formatter = new Intl.NumberFormat(undefined, { + style: "percent", + ...options, + }); + return computed(() => formatter.format(number.value)); +} diff --git a/src/ui/src/core/templateMap.ts b/src/ui/src/core/templateMap.ts index c3031ccbd..ec4b344f2 100644 --- a/src/ui/src/core/templateMap.ts +++ b/src/ui/src/core/templateMap.ts @@ -17,6 +17,7 @@ import CoreTags from "../components/core/content/CoreTags.vue"; import CoreAvatar from "../components/core/content/CoreAvatar.vue"; import CoreAnnotatedText from "../components/core/content/CoreAnnotatedText.vue"; import CoreJsonViewer from "../components/core/content/CoreJsonViewer.vue"; +import CoreProgressBar from "../components/core/content/CoreProgressBar.vue"; // input import CoreCheckboxInput from "../components/core/input/CoreCheckboxInput.vue"; @@ -136,6 +137,7 @@ const templateMap: TemplateMap = { jsonviewer: CoreJsonViewer, workflows_root: WorkflowsRoot, workflows_workflow: WorkflowsWorkflow, + progressbar: CoreProgressBar, }; const abstractTemplateMap: Record = {}; diff --git a/src/ui/src/renderer/syntheticEvents.ts b/src/ui/src/renderer/syntheticEvents.ts index df7bab08c..5fcf3b229 100644 --- a/src/ui/src/renderer/syntheticEvents.ts +++ b/src/ui/src/renderer/syntheticEvents.ts @@ -1,4 +1,7 @@ -export function getClick(ev: MouseEvent): CustomEvent { +/** + * @param ev event from a mouse click, or a keyboard click (using tab navigation, then click with `Enter`) + */ +export function getClick(ev: MouseEvent | KeyboardEvent): CustomEvent { const payload = { ctrlKey: ev.ctrlKey, shiftKey: ev.shiftKey, diff --git a/src/ui/src/renderer/useFormValueBroker.ts b/src/ui/src/renderer/useFormValueBroker.ts index 90d8ce803..cc854b686 100644 --- a/src/ui/src/renderer/useFormValueBroker.ts +++ b/src/ui/src/renderer/useFormValueBroker.ts @@ -3,17 +3,19 @@ import { useEvaluator } from "@/renderer/useEvaluator"; import { Core, InstancePath } from "@/writerTypes"; /** - * * Encapsulates repeatable form value logic, including binding. * * @param wf * @param componentId + * @param defaultValue the initial value when binding is not set * @returns */ export function useFormValueBroker( wf: Core, instancePath: InstancePath, emitterEl: Ref, + // @ts-expect-error keep default string for compatibility reason + defaultValue: T = "", ) { const formValue: Ref = ref(); const isBusy = ref(false); @@ -110,7 +112,7 @@ export function useFormValueBroker( formValue, (newValue) => { if (typeof newValue === "undefined") { - formValue.value = "" as T; + formValue.value = defaultValue; } }, { immediate: true }, diff --git a/src/writer/core_ui.py b/src/writer/core_ui.py index a380a07a0..63d23e329 100644 --- a/src/writer/core_ui.py +++ b/src/writer/core_ui.py @@ -6,7 +6,7 @@ from enum import Enum from typing import Any, Dict, List, Literal, Optional, Union, cast -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, field_validator, validator from typing_extensions import TypedDict from writer.ss_types import ComponentDefinition, ServeMode @@ -57,6 +57,12 @@ class Component(BaseModel): x: Optional[int] = None y: Optional[int] = None + @field_validator("x", "y", mode="before") + def cast_float_to_int(cls, v): + if isinstance(v, float): + return int(v) + return v + def to_dict(self) -> Dict: """ Wrapper for model_dump to ensure backward compatibility.