Skip to content

Commit 08e4c46

Browse files
authored
Cleaning up span/task creation with tags and better names. (#232)
Refactor xml_example to take field info like examples and descriptions into account. Rework to_pretty_xml to render multi-line text better. Add some better __str__ logic for usage, tool calls, and chats Fix multi-line text processing so they aren't over indented when parsed. - Update docs update workflow
1 parent 93fc7e2 commit 08e4c46

File tree

20 files changed

+783
-271
lines changed

20 files changed

+783
-271
lines changed

.github/workflows/docs-update.yaml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
name: Notify Documentation Update
2+
name: Trigger Docs Update
33

44
on:
55
push:
@@ -21,15 +21,14 @@ jobs:
2121
private-key: ${{ secrets.UPDATE_DOCS_PRIVATE_KEY }}
2222
owner: "${{ github.repository_owner }}"
2323
repositories: |
24-
sdk
25-
prod-docs
24+
docs
2625
2726
- name: Trigger docs repository workflow
2827
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0
2928
with:
3029
token: ${{ steps.app-token.outputs.token }}
31-
repository: dreadnode/prod-docs
32-
event-type: code-update
30+
repository: dreadnode/docs
31+
event-type: docs-update
3332
client-payload: |
3433
{
3534
"repository": "${{ github.repository }}",

docs/api/chat.mdx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1263,7 +1263,7 @@ def __init__(
12631263
"""How to handle failures in the pipeline unless overridden in calls."""
12641264
self.caching: CacheMode | None = None
12651265
"""How to handle cache_control entries on messages."""
1266-
self.task_name: str = generator.to_identifier(short=True)
1266+
self.task_name: str = f"Chat with {generator.to_identifier(short=True)}"
12671267
"""The name of the pipeline task, used for logging and debugging."""
12681268
self.scorers: list[Scorer[Chat]] = []
12691269
"""List of dreadnode scorers to evaluate the generated chat upon completion."""
@@ -1367,7 +1367,7 @@ List of dreadnode scorers to evaluate the generated chat upon completion.
13671367
### task\_name
13681368

13691369
```python
1370-
task_name: str = to_identifier(short=True)
1370+
task_name: str = f'Chat with {to_identifier(short=True)}'
13711371
```
13721372

13731373
The name of the pipeline task, used for logging and debugging.
@@ -1942,7 +1942,7 @@ def map(
19421942

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

19481948
self.map_callbacks.extend([(callback, max_depth, as_task) for callback in callbacks])
@@ -2153,8 +2153,9 @@ async def run(
21532153

21542154
last: PipelineStep | None = None
21552155
with dn.task_span(
2156-
name or f"pipeline - {self.task_name}",
2156+
name or self.task_name,
21572157
label=name or f"pipeline_{self.task_name}",
2158+
tags=["rigging/pipeline"],
21582159
attributes={"rigging.type": "chat_pipeline.run"},
21592160
) as task:
21602161
dn.log_inputs(
@@ -2286,8 +2287,9 @@ async def run_batch(
22862287

22872288
last: PipelineStep | None = None
22882289
with dn.task_span(
2289-
name or f"pipeline - {self.task_name} (batch x{count})",
2290+
name or f"{self.task_name} (batch x{count})",
22902291
label=name or f"pipeline_batch_{self.task_name}",
2292+
tags=["rigging/pipeline"],
22912293
attributes={"rigging.type": "chat_pipeline.run_batch"},
22922294
) as task:
22932295
dn.log_inputs(
@@ -2433,8 +2435,9 @@ async def run_many(
24332435

24342436
last: PipelineStep | None = None
24352437
with dn.task_span(
2436-
name or f"pipeline - {self.task_name} (x{count})",
2438+
name or f"{self.task_name} (x{count})",
24372439
label=name or f"pipeline_many_{self.task_name}",
2440+
tags=["rigging/pipeline"],
24382441
attributes={"rigging.type": "chat_pipeline.run_many"},
24392442
) as task:
24402443
dn.log_inputs(
@@ -2975,7 +2978,7 @@ def then(
29752978

29762979
if callback in [c[0] for c in self.then_callbacks]:
29772980
raise ValueError(
2978-
f"Callback '{get_qualified_name(callback)}' is already registered.",
2981+
f"Callback '{get_callable_name(callback)}' is already registered.",
29792982
)
29802983

29812984
self.then_callbacks.extend([(callback, max_depth, as_task) for callback in callbacks])
@@ -3075,7 +3078,7 @@ def transform(
30753078
for callback in callbacks:
30763079
if not allow_duplicates and callback in self.transforms:
30773080
raise ValueError(
3078-
f"Callback '{get_qualified_name(callback)}' is already registered.",
3081+
f"Callback '{get_callable_name(callback)}' is already registered.",
30793082
)
30803083

30813084
self.transforms.extend(callbacks)
@@ -3473,7 +3476,7 @@ def watch(
34733476
for callback in callbacks:
34743477
if not allow_duplicates and callback in self.watch_callbacks:
34753478
raise ValueError(
3476-
f"Callback '{get_qualified_name(callback)}' is already registered.",
3479+
f"Callback '{get_callable_name(callback)}' is already registered.",
34773480
)
34783481

34793482
self.watch_callbacks.extend(callbacks)

docs/api/message.mdx

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1951,6 +1951,51 @@ def replace_with_slice(
19511951
```
19521952

19531953

1954+
</Accordion>
1955+
1956+
### shorten
1957+
1958+
```python
1959+
shorten(max_length: int, sep: str = '...') -> Message
1960+
```
1961+
1962+
Shortens the message content to at most max\_length characters long by removing the middle of the string
1963+
1964+
**Parameters:**
1965+
1966+
* **`max_length`**
1967+
(`int`)
1968+
–The maximum length of the message content.
1969+
* **`sep`**
1970+
(`str`, default:
1971+
`'...'`
1972+
)
1973+
–The separator to use when shortening the content.
1974+
1975+
**Returns:**
1976+
1977+
* `Message`
1978+
–The shortened message.
1979+
1980+
<Accordion title="Source code in rigging/message.py" icon="code">
1981+
```python
1982+
def shorten(self, max_length: int, sep: str = "...") -> "Message":
1983+
"""
1984+
Shortens the message content to at most max_length characters long by removing the middle of the string
1985+
1986+
Args:
1987+
max_length: The maximum length of the message content.
1988+
sep: The separator to use when shortening the content.
1989+
1990+
Returns:
1991+
The shortened message.
1992+
"""
1993+
new = self.clone()
1994+
new.content = shorten_string(new.content, max_length, sep=sep)
1995+
return new
1996+
```
1997+
1998+
19541999
</Accordion>
19552000

19562001
### strip
@@ -2469,8 +2514,9 @@ Returns a string representation of the slice.
24692514
```python
24702515
def __str__(self) -> str:
24712516
"""Returns a string representation of the slice."""
2472-
content_preview = self.content if self._message else "[detached]"
2473-
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)}'>"
2517+
content = shorten_string(self.content if self._message else "[detached]", 50)
2518+
obj = self.obj.__class__.__name__ if self.obj else None
2519+
return f"MessageSlice(type='{self.type}', start={self.start}, stop={self.stop} obj={obj} content='{content}')"
24742520
```
24752521

24762522

docs/api/model.mdx

Lines changed: 72 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,13 @@ def from_text(
207207

208208
try:
209209
model = (
210-
cls(**{next(iter(cls.model_fields)): unescape_xml(inner)})
210+
cls(
211+
**{
212+
next(iter(cls.model_fields)): unescape_xml(
213+
textwrap.dedent(inner).strip()
214+
)
215+
}
216+
)
211217
if cls.is_simple()
212218
else cls.from_xml(
213219
cls.preprocess_with_cdata(full_text),
@@ -217,7 +223,7 @@ def from_text(
217223
# If our model is relatively simple (only attributes and a single non-element field)
218224
# we should go back and update our non-element field with the extracted content.
219225

220-
if cls.is_simple_with_attrs():
226+
if not cls.is_simple() and cls.is_simple_with_attrs():
221227
name, field = next(
222228
(name, field)
223229
for name, field in cls.model_fields.items()
@@ -228,6 +234,14 @@ def from_text(
228234
unescape_xml(inner).strip(),
229235
)
230236

237+
# Walk through any fields which are strings, and dedent them
238+
239+
for field_name, field_info in cls.model_fields.items():
240+
if isinstance(field_info, XmlEntityInfo) and field_info.annotation == str: # noqa: E721
241+
model.__dict__[field_name] = textwrap.dedent(
242+
model.__dict__[field_name]
243+
).strip()
244+
231245
extracted.append((model, slice_))
232246
except Exception as e: # noqa: BLE001
233247
extracted.append((e, slice_))
@@ -485,7 +499,7 @@ def preprocess_with_cdata(cls, content: str) -> str:
485499
needs_escaping = escape_xml(unescape_xml(content)) != content
486500

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

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

@@ -514,7 +528,7 @@ to_pretty_xml(
514528
skip_empty: bool = False,
515529
exclude_none: bool = False,
516530
exclude_unset: bool = False,
517-
**kwargs: Any,
531+
**_: Any,
518532
) -> str
519533
```
520534

@@ -533,7 +547,7 @@ def to_pretty_xml(
533547
skip_empty: bool = False,
534548
exclude_none: bool = False,
535549
exclude_unset: bool = False,
536-
**kwargs: t.Any,
550+
**_: t.Any,
537551
) -> str:
538552
"""
539553
Converts the model to a pretty XML string with indents and newlines.
@@ -546,22 +560,7 @@ def to_pretty_xml(
546560
exclude_none=exclude_none,
547561
exclude_unset=exclude_unset,
548562
)
549-
tree = self._postprocess_with_cdata(tree)
550-
551-
ET.indent(tree, " ")
552-
pretty_encoded_xml = str(
553-
ET.tostring(
554-
tree,
555-
short_empty_elements=False,
556-
encoding="utf-8",
557-
**kwargs,
558-
).decode(),
559-
)
560-
561-
# Now we can go back and safely unescape the XML
562-
# that we observe between any CDATA tags
563-
564-
return unescape_cdata_tags(pretty_encoded_xml)
563+
return self._serialize_tree_prettily(tree)
565564
```
566565

567566

@@ -676,14 +675,19 @@ xml_example() -> str
676675

677676
Returns an example XML representation of the given class.
678677

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

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

683687
**Returns:**
684688

685689
* `str`
686-
–A string containing the XML representation of the class.
690+
–A string containing the pretty-printed XML example.
687691

688692
<Accordion title="Source code in rigging/model.py" icon="code">
689693
```python
@@ -692,27 +696,55 @@ def xml_example(cls) -> str:
692696
"""
693697
Returns an example XML representation of the given class.
694698
695-
Models should typically override this method to provide a more complex example.
699+
This method generates a pretty-printed XML string that includes:
700+
- Example values for each field, taken from the `example` argument
701+
in a field constructor.
702+
- Field descriptions as XML comments, derived from the field's
703+
docstring or the `description` argument.
696704
697-
By default, this method returns a hollow XML scaffold one layer deep.
705+
Note: This implementation is designed for models with flat structures
706+
and does not recursively generate examples for nested models.
698707
699708
Returns:
700-
A string containing the XML representation of the class.
709+
A string containing the pretty-printed XML example.
701710
"""
702711
if cls.is_simple():
703-
return cls.xml_tags()
704-
705-
schema = cls.model_json_schema()
706-
properties = schema["properties"]
707-
structure = {cls.__xml_tag__: dict.fromkeys(properties)}
708-
xml_string = xmltodict.unparse(
709-
structure,
710-
pretty=True,
711-
full_document=False,
712-
indent=" ",
713-
short_empty_elements=True,
714-
)
715-
return t.cast("str", xml_string) # Bad type hints in xmltodict
712+
field_info = next(iter(cls.model_fields.values()))
713+
example = str(next(iter(field_info.examples or []), ""))
714+
return f"<{cls.__xml_tag__}>{escape_xml(example)}</{cls.__xml_tag__}>"
715+
716+
lines = []
717+
attribute_parts = []
718+
element_fields = {}
719+
720+
for field_name, field_info in cls.model_fields.items():
721+
if (
722+
isinstance(field_info, XmlEntityInfo)
723+
and field_info.location == EntityLocation.ATTRIBUTE
724+
):
725+
path = field_info.path or field_name
726+
example = str(next(iter(field_info.examples or []), "")).replace('"', "&quot;")
727+
attribute_parts.append(f'{path}="{example}"')
728+
else:
729+
element_fields[field_name] = field_info
730+
731+
attr_string = (" " + " ".join(attribute_parts)) if attribute_parts else ""
732+
lines.append(f"<{cls.__xml_tag__}{attr_string}>")
733+
734+
for field_name, field_info in element_fields.items():
735+
path = (isinstance(field_info, XmlEntityInfo) and field_info.path) or field_name
736+
description = field_info.description
737+
example = str(next(iter(field_info.examples or []), ""))
738+
739+
if description:
740+
lines.append(f" <!-- {escape_xml(description.strip())} -->")
741+
if example:
742+
lines.append(f" <{path}>{escape_xml(example)}</{path}>")
743+
else:
744+
lines.append(f" <{path}/>")
745+
746+
lines.append(f"</{cls.__xml_tag__}>")
747+
return "\n".join(lines)
716748
```
717749

718750

0 commit comments

Comments
 (0)