def add(a,b):
return a+b
class Pow():
def __init__(self,power):
self.power = power
def __call__(self,a):
return a**self.power
def __str__(self):
return f'Pow({self.power})'
class Add():
def __init__(self,a):
self.a = a
def __call__(self,b):
return self.a+b
def __str__(self):
return f'Add({self.a})'
class IsModulo():
def __init__(self,mod):
self.mod = mod
def __call__(self,a):
return a%self.mod==0
def __str__(self):
return f'IsModulo({self.mod})'
class EqualsTo():
def __init__(self,value):
self.value = value
def __call__(self,a):
return a==self.value
def __str__(self):
return f'EqualsTo({self.value})'Compound Diagrams
Sometimes, we would like to combine parrallel flows with conditional logic in one Diagram. While this could always be performed with nested diagrams, it is also possible to create compound diagrams with the use of Scopes.
# note the new import
from stringdale import Define,V,E,Condition,ScopeFlow in Decision
We define a diagram as follows:
with Define('parallel in decision',type='decision') as D:
V('parallel_start',inputs=['Start'])
# all edges defined within a flow scope are automatically added as flow edges
with Scope('flow'):
V('square',Pow(2),inputs=['parallel_start'])
V('cube',Pow(3),inputs=['parallel_start'])
V('add',add,inputs=['square(a)','cube(b)'])
# we can override the scope and define an edge type explicitly
E('add->parallel_start',cond=EqualsTo(12),type='decision')
E('add->End')
D.draw()A legal compound diagram must have a single start and end node for each sub scope. In this case, parallel_start and add are the start and end of the flow scope respectivly.
d = D()
for trace in d.run(3):
trace.pprint()---
name: Start
input:
0: 3
output: 3
---
name: parallel_start
input:
0: 3
output: 3
---
name: square
input:
0: 3
output: 9
---
name: cube
input:
0: 3
output: 27
---
name: add
input:
a: 9
b: 27
output: 36
---
name: End
input:
0: 36
output: 36
d = D()
for trace in d.run(2):
trace.pprint()---
name: Start
input:
0: 2
output: 2
---
name: parallel_start
input:
0: 2
output: 2
---
name: square
input:
0: 2
output: 4
---
name: cube
input:
0: 2
output: 8
---
name: add
input:
a: 4
b: 8
output: 12
---
name: parallel_start
input:
0: 12
output: 12
---
name: square
input:
0: 12
output: 144
---
name: cube
input:
0: 12
output: 1728
---
name: add
input:
a: 144
b: 1728
output: 1872
---
name: End
input:
0: 1872
output: 1872
Decision in Flow
We are not limited to a single sub scope. We can nest them as deep as we want, though above a certain size, its better to use nested diagrams for debug-ability.
with Define('parallel decisions',type='flow') as D:
V('Start',outputs=['choice1','choice2'])
with Scope('decision'):
V('choice1',outputs=[
('square',IsModulo(2)),
'cube'
])
V('square',Pow(2),outputs=['end_choice1'])
V('cube',Pow(3),outputs=['end_choice1'])
V('choice2',outputs=[
('add2',IsModulo(3)),
'add3'
])
V('add2',Add(2),outputs=['end_choice2'])
V('add3',Add(3),outputs=['end_choice2'])
V('sum',add,inputs=['end_choice1(a)','end_choice2(b)'],outputs=['End'])
D.draw()from stringdale.core import checkLogsNote that even though we defined them in a single decision Scope, we have actually created 2 decision sub scopes.
d=D()
for trace in d.run(2):
trace.pprint(skip_passthrough=True)---
name: add3
input:
0: 2
output: 5
---
name: square
input:
0: 2
output: 4
---
name: sum
input:
b: 5
a: 4
output: 9
d=D()
for trace in d.run(3):
trace.pprint(skip_passthrough=True)---
name: cube
input:
0: 3
output: 27
---
name: add2
input:
0: 3
output: 5
---
name: sum
input:
b: 5
a: 27
output: 32
Example - Simplifying routing
Let us recall our routing workflow from the decision diagrams tutorial.
from stringdale.chat import Chatrhyming_agent = Chat(model='gpt-4o-mini',
messages=[{'role':'system','content':"""
Answer the following questions using rhyming words.
"""},
{'role':'user','content':'{{question}}'},
],
)
joke_agent = Chat(model='gpt-4o-mini',
messages=[{'role':'system','content':"""
Answer the following question with a joke.
"""},
{'role':'user','content':'{{question}}'},
])
yo_mama_chat = Chat(model='gpt-4o-mini',
messages=[{'role':'system','content':"""
Answer the following question with a joke about the person's mother.
"""},
{'role':'user','content':'{{question}}'},
])
choice_descriptions = {
'rhyme':'this agent is good at rhyming',
'joke': 'this agent is good at telling jokes',
'yo_mama': 'this agent is specifically good at telling jokes about mothers'
}
router = Chat(model='gpt-4o-mini',
messages=[{'role':'system','content':"""
Choose the best sub-agent to answer the following question from among the following options:
{% for name,description in choice_descriptions.items() %}
- {{name}}: {{description}}
{% endfor %}
"""},
{'role':'user','content':'{{question}}'},
],
choice_descriptions = choice_descriptions,
choices=list(choice_descriptions.keys())
)with Define('Router',type='decision') as Router:
E('Start->state/q')
V('router',router,
inputs=['Start(question=.)'],
outputs=[
('rhyme(_)',),
('joke(_)',Condition('joke','(0=content)',name='content==joke')),
('yo_mama(_)',Condition('yo_mama','(0=content)',name='content==yo_mama')),
]
)
V('rhyme',rhyming_agent,
inputs=['state/q(question=.)'],
outputs=['End']
)
V('joke',joke_agent,
inputs=['state/q(question=.)'],
outputs=['End']
)
V('yo_mama',yo_mama_chat,
inputs=['state/q(question=.)'],
outputs=['End']
)
Router.draw()With a flow scope, we can avoid all of these state edges without resorting to nested diagrams.
with Define('Router',type='decision') as CompoundRouter:
with Scope('flow'):
V('choose_route',router,inputs=['Start(question=.)'])
V('router',
inputs=[
'choose_route(choice=content)',
'Start(question=.)'
],)
E('router->rhyme(question=question)')
E('router->joke(question=question)',cond=Condition('joke','(0=choice)',name='choice==joke'))
E('router->yo_mama(question=question)',cond=Condition('yo_mama','(0=choice)',name='choice==yo_mama'))
V('rhyme',rhyming_agent,outputs=['End'])
V('joke',joke_agent,outputs=['End'])
V('yo_mama',yo_mama_chat,outputs=['End'])
CompoundRouter.draw()d = CompoundRouter()
for trace in d.run('what is the capital of france? I like rhyming'):
trace.pprint(skip_passthrough=True)---
name: choose_route
input:
question: what is the capital of france? I like rhyming
output:
role: assistant
content: rhyme
meta:
input_tokens: 203
output_tokens: 11
---
name: rhyme
input:
question: what is the capital of france? I like rhyming
output:
role: assistant
content: |-
The capital of France, oh what a dance, Is Paris, a place that gives dreams a
chance. With lights that glow and art that flows, It captures the heart and
forever bestows.
meta:
input_tokens: 33
output_tokens: 42