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.