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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions .github/workflows/docs-update.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
name: Notify Documentation Update
name: Trigger Docs Update

on:
push:
Expand All @@ -21,15 +21,14 @@ jobs:
private-key: ${{ secrets.UPDATE_DOCS_PRIVATE_KEY }}
owner: "${{ github.repository_owner }}"
repositories: |
sdk
prod-docs
docs

- name: Trigger docs repository workflow
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0
with:
token: ${{ steps.app-token.outputs.token }}
repository: dreadnode/prod-docs
event-type: code-update
repository: dreadnode/docs
event-type: docs-update
client-payload: |
{
"repository": "${{ github.repository }}",
Expand Down
21 changes: 12 additions & 9 deletions docs/api/chat.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1263,7 +1263,7 @@ def __init__(
"""How to handle failures in the pipeline unless overridden in calls."""
self.caching: CacheMode | None = None
"""How to handle cache_control entries on messages."""
self.task_name: str = generator.to_identifier(short=True)
self.task_name: str = f"Chat with {generator.to_identifier(short=True)}"
"""The name of the pipeline task, used for logging and debugging."""
self.scorers: list[Scorer[Chat]] = []
"""List of dreadnode scorers to evaluate the generated chat upon completion."""
Expand Down Expand Up @@ -1360,7 +1360,7 @@ List of dreadnode scorers to evaluate the generated chat upon completion.
### task\_name

```python
task_name: str = to_identifier(short=True)
task_name: str = f'Chat with {to_identifier(short=True)}'
```

The name of the pipeline task, used for logging and debugging.
Expand Down Expand Up @@ -1935,7 +1935,7 @@ def map(

if callback in [c[0] for c in self.map_callbacks]:
raise ValueError(
f"Callback '{get_qualified_name(callback)}' is already registered.",
f"Callback '{get_callable_name(callback)}' is already registered.",
)

self.map_callbacks.extend([(callback, max_depth, as_task) for callback in callbacks])
Expand Down Expand Up @@ -2146,8 +2146,9 @@ async def run(

last: PipelineStep | None = None
with dn.task_span(
name or f"pipeline - {self.task_name}",
name or self.task_name,
label=name or f"pipeline_{self.task_name}",
tags=["rigging/pipeline"],
attributes={"rigging.type": "chat_pipeline.run"},
) as task:
dn.log_inputs(
Expand Down Expand Up @@ -2279,8 +2280,9 @@ async def run_batch(

last: PipelineStep | None = None
with dn.task_span(
name or f"pipeline - {self.task_name} (batch x{count})",
name or f"{self.task_name} (batch x{count})",
label=name or f"pipeline_batch_{self.task_name}",
tags=["rigging/pipeline"],
attributes={"rigging.type": "chat_pipeline.run_batch"},
) as task:
dn.log_inputs(
Expand Down Expand Up @@ -2426,8 +2428,9 @@ async def run_many(

last: PipelineStep | None = None
with dn.task_span(
name or f"pipeline - {self.task_name} (x{count})",
name or f"{self.task_name} (x{count})",
label=name or f"pipeline_many_{self.task_name}",
tags=["rigging/pipeline"],
attributes={"rigging.type": "chat_pipeline.run_many"},
) as task:
dn.log_inputs(
Expand Down Expand Up @@ -2968,7 +2971,7 @@ def then(

if callback in [c[0] for c in self.then_callbacks]:
raise ValueError(
f"Callback '{get_qualified_name(callback)}' is already registered.",
f"Callback '{get_callable_name(callback)}' is already registered.",
)

self.then_callbacks.extend([(callback, max_depth, as_task) for callback in callbacks])
Expand Down Expand Up @@ -3068,7 +3071,7 @@ def transform(
for callback in callbacks:
if not allow_duplicates and callback in self.transforms:
raise ValueError(
f"Callback '{get_qualified_name(callback)}' is already registered.",
f"Callback '{get_callable_name(callback)}' is already registered.",
)

self.transforms.extend(callbacks)
Expand Down Expand Up @@ -3442,7 +3445,7 @@ def watch(
for callback in callbacks:
if not allow_duplicates and callback in self.watch_callbacks:
raise ValueError(
f"Callback '{get_qualified_name(callback)}' is already registered.",
f"Callback '{get_callable_name(callback)}' is already registered.",
)

self.watch_callbacks.extend(callbacks)
Expand Down
50 changes: 48 additions & 2 deletions docs/api/message.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1924,6 +1924,51 @@ def replace_with_slice(
```


</Accordion>

### shorten

```python
shorten(max_length: int, sep: str = '...') -> Message
```

Shortens the message content to at most max\_length characters long by removing the middle of the string

**Parameters:**

* **`max_length`**
(`int`)
–The maximum length of the message content.
* **`sep`**
(`str`, default:
`'...'`
)
–The separator to use when shortening the content.

**Returns:**

* `Message`
–The shortened message.

<Accordion title="Source code in rigging/message.py" icon="code">
```python
def shorten(self, max_length: int, sep: str = "...") -> "Message":
"""
Shortens the message content to at most max_length characters long by removing the middle of the string

Args:
max_length: The maximum length of the message content.
sep: The separator to use when shortening the content.

Returns:
The shortened message.
"""
new = self.clone()
new.content = shorten_string(new.content, max_length, sep=sep)
return new
```


</Accordion>

### strip
Expand Down Expand Up @@ -2442,8 +2487,9 @@ Returns a string representation of the slice.
```python
def __str__(self) -> str:
"""Returns a string representation of the slice."""
content_preview = self.content if self._message else "[detached]"
return f"<MessageSlice type='{self.type}' start={self.start} stop={self.stop} obj={self.obj.__class__.__name__ if self.obj else None} content='{shorten_string(content_preview, 50)}'>"
content = shorten_string(self.content if self._message else "[detached]", 50)
obj = self.obj.__class__.__name__ if self.obj else None
return f"MessageSlice(type='{self.type}', start={self.start}, stop={self.stop} obj={obj} content='{content}')"
```


Expand Down
112 changes: 72 additions & 40 deletions docs/api/model.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,13 @@ def from_text(

try:
model = (
cls(**{next(iter(cls.model_fields)): unescape_xml(inner)})
cls(
**{
next(iter(cls.model_fields)): unescape_xml(
textwrap.dedent(inner).strip()
)
}
)
if cls.is_simple()
else cls.from_xml(
cls.preprocess_with_cdata(full_text),
Expand All @@ -217,7 +223,7 @@ def from_text(
# If our model is relatively simple (only attributes and a single non-element field)
# we should go back and update our non-element field with the extracted content.

if cls.is_simple_with_attrs():
if not cls.is_simple() and cls.is_simple_with_attrs():
name, field = next(
(name, field)
for name, field in cls.model_fields.items()
Expand All @@ -228,6 +234,14 @@ def from_text(
unescape_xml(inner).strip(),
)

# Walk through any fields which are strings, and dedent them

for field_name, field_info in cls.model_fields.items():
if isinstance(field_info, XmlEntityInfo) and field_info.annotation == str: # noqa: E721
model.__dict__[field_name] = textwrap.dedent(
model.__dict__[field_name]
).strip()

extracted.append((model, slice_))
except Exception as e: # noqa: BLE001
extracted.append((e, slice_))
Expand Down Expand Up @@ -485,7 +499,7 @@ def preprocess_with_cdata(cls, content: str) -> str:
needs_escaping = escape_xml(unescape_xml(content)) != content

if is_basic_field and not is_already_cdata and needs_escaping:
content = f"<![CDATA[{content}]]>"
content = f"<![CDATA[{textwrap.dedent(content).strip()}]]>"

return f"<{field_name}{tag_attrs}>{content}</{field_name}>"

Expand Down Expand Up @@ -514,7 +528,7 @@ to_pretty_xml(
skip_empty: bool = False,
exclude_none: bool = False,
exclude_unset: bool = False,
**kwargs: Any,
**_: Any,
) -> str
```

Expand All @@ -533,7 +547,7 @@ def to_pretty_xml(
skip_empty: bool = False,
exclude_none: bool = False,
exclude_unset: bool = False,
**kwargs: t.Any,
**_: t.Any,
) -> str:
"""
Converts the model to a pretty XML string with indents and newlines.
Expand All @@ -546,22 +560,7 @@ def to_pretty_xml(
exclude_none=exclude_none,
exclude_unset=exclude_unset,
)
tree = self._postprocess_with_cdata(tree)

ET.indent(tree, " ")
pretty_encoded_xml = str(
ET.tostring(
tree,
short_empty_elements=False,
encoding="utf-8",
**kwargs,
).decode(),
)

# Now we can go back and safely unescape the XML
# that we observe between any CDATA tags

return unescape_cdata_tags(pretty_encoded_xml)
return self._serialize_tree_prettily(tree)
```


Expand Down Expand Up @@ -676,14 +675,19 @@ xml_example() -> str

Returns an example XML representation of the given class.

Models should typically override this method to provide a more complex example.
This method generates a pretty-printed XML string that includes:
- Example values for each field, taken from the `example` argument
in a field constructor.
- Field descriptions as XML comments, derived from the field's
docstring or the `description` argument.

By default, this method returns a hollow XML scaffold one layer deep.
Note: This implementation is designed for models with flat structures
and does not recursively generate examples for nested models.

**Returns:**

* `str`
–A string containing the XML representation of the class.
–A string containing the pretty-printed XML example.

<Accordion title="Source code in rigging/model.py" icon="code">
```python
Expand All @@ -692,27 +696,55 @@ def xml_example(cls) -> str:
"""
Returns an example XML representation of the given class.

Models should typically override this method to provide a more complex example.
This method generates a pretty-printed XML string that includes:
- Example values for each field, taken from the `example` argument
in a field constructor.
- Field descriptions as XML comments, derived from the field's
docstring or the `description` argument.

By default, this method returns a hollow XML scaffold one layer deep.
Note: This implementation is designed for models with flat structures
and does not recursively generate examples for nested models.

Returns:
A string containing the XML representation of the class.
A string containing the pretty-printed XML example.
"""
if cls.is_simple():
return cls.xml_tags()

schema = cls.model_json_schema()
properties = schema["properties"]
structure = {cls.__xml_tag__: dict.fromkeys(properties)}
xml_string = xmltodict.unparse(
structure,
pretty=True,
full_document=False,
indent=" ",
short_empty_elements=True,
)
return t.cast("str", xml_string) # Bad type hints in xmltodict
field_info = next(iter(cls.model_fields.values()))
example = str(next(iter(field_info.examples or []), ""))
return f"<{cls.__xml_tag__}>{escape_xml(example)}</{cls.__xml_tag__}>"

lines = []
attribute_parts = []
element_fields = {}

for field_name, field_info in cls.model_fields.items():
if (
isinstance(field_info, XmlEntityInfo)
and field_info.location == EntityLocation.ATTRIBUTE
):
path = field_info.path or field_name
example = str(next(iter(field_info.examples or []), "")).replace('"', "&quot;")
attribute_parts.append(f'{path}="{example}"')
else:
element_fields[field_name] = field_info

attr_string = (" " + " ".join(attribute_parts)) if attribute_parts else ""
lines.append(f"<{cls.__xml_tag__}{attr_string}>")

for field_name, field_info in element_fields.items():
path = (isinstance(field_info, XmlEntityInfo) and field_info.path) or field_name
description = field_info.description
example = str(next(iter(field_info.examples or []), ""))

if description:
lines.append(f" <!-- {escape_xml(description.strip())} -->")
if example:
lines.append(f" <{path}>{escape_xml(example)}</{path}>")
else:
lines.append(f" <{path}/>")

lines.append(f"</{cls.__xml_tag__}>")
return "\n".join(lines)
```


Expand Down
Loading
Loading