Some Basics¶
In this recipe, we use a “counting” cogent to illustrate basic usage of a few library features. For each cogent run, it is supplied a starting integer - the cogent then increments until it arrives at a “perfect square” (a number you get when you multiply a whole number by itself). We focus on the following new components…
Elaboration: information, accessible via
IOContainer.e.feature_name, that is computed each cycle (during the DPElaborationphase)This will perform the math to identify a perfect square (independent of other logic)
Also,
create_elaborator: a convenience function for creating anElaboratorbased upon kwargs that associate feature_name with a function to compute the associated value
Termination Check: a function that (during the DP
TerminationCheckphase) determines if the DP should endUnlike some other recipes, it is not the application of an operator that signals termination, but instead a property of the DP state (namely, having achieved a perfect square)
Arguments: read-only name=value pairs supplied to a DP that are accessible via
IOContainer.a.nameIn this case, the starting value
Logging: using the standard Python
logginglibraryDebug output: using the
stringifyfunction can improve readability of logs, as well as debug output (e.g., printing a DP)
Code¶
1"""
2Illustrates a simple counting agent that
3uses a termination check based upon the
4result of an elaborator
5"""
6
7import logging
8from dataclasses import dataclass
9from math import sqrt
10from typing import cast
11
12from cognition import (
13 Cogent,
14 DecisionProcess,
15 IOContainer,
16 Operator,
17 create_elaborator,
18 stringify,
19)
20
21# ===
22
23# Example of enabling logging to the console (stdout),
24# capturing only those entries that rise to
25# the level of a warning (rare)
26
27handler = logging.StreamHandler() # options exist for files, http, etc.
28handler.setFormatter(
29 logging.Formatter(
30 "%(levelname)s\t%(name)s\t%(asctime)s\t%(message)s"
31 ) # more fields exist
32)
33
34logger = logging.getLogger("cognition") # can be set to a sub-package for focus
35logger.setLevel(logging.WARNING) # set to DEBUG for details
36logger.addHandler(handler)
37
38# ===
39
40
41@dataclass
42class CountingState:
43 """
44 keeps track of where we are in the
45 counting process (init, or current value)
46 """
47
48 counter: int | None = None
49 """
50 current value: None signals to init,
51 otherwise current count
52 """
53
54
55def _is_perfect_square(num: int) -> bool:
56 return sqrt(num) % 1 == 0
57
58
59assert not _is_perfect_square(8)
60assert _is_perfect_square(9)
61assert not _is_perfect_square(10)
62
63
64@stringify("is_perfect") # easier-to-read in logs/debug output
65def is_perfect_square(cs: CountingState, _io: IOContainer) -> bool:
66 """
67 example elaboration:
68 examine raw counter state to determine if the value is a perfect square?
69 """
70
71 return (cs.counter is not None) and _is_perfect_square(cs.counter)
72
73
74# ===
75
76# note that the dp state "initializer" in this
77# case is just the class constructor
78counting_cogent = Cogent(DecisionProcess(CountingState))
79
80# these could very well be methods of the state class,
81# but for illustration, this will make available three
82# keys on io.e.kwarg that automatically update each cycle
83# during the elaboration phase
84counting_cogent.dp.add_elaborator(
85 create_elaborator(
86 "examples", # only visible in logs/debug output
87 is_initialized=lambda cs, _: cs.counter is not None,
88 is_perfect=is_perfect_square,
89 as_str=lambda cs, _: str(cs.counter),
90 )
91)
92
93
94@counting_cogent.dp.termination_check
95@stringify("found_perfect")
96def found_perfect(_cs: CountingState, io: IOContainer) -> bool:
97 """
98 utilizes elaboration to detect custom termination logic:
99 in this case that a perfect square has been achieved
100 """
101
102 print(f"Terminate?: {io.e.as_str=} ({io.e.is_perfect=})")
103
104 return cast(bool, io.e.is_perfect)
105
106
107# ===
108
109
110@counting_cogent.dp.operator("init")
111class InitOperator(Operator[CountingState]):
112 """
113 sets the initial counter value
114 based upon a supplied argument
115 """
116
117 def can_perform(self, _state: CountingState, io: IOContainer) -> bool:
118 return not cast(bool, io.e.is_initialized)
119
120 def perform(self, state: CountingState, io: IOContainer) -> None:
121 start_val = cast(int, io.a.counter_start)
122
123 print(f"Initialize: {start_val}")
124
125 state.counter = start_val
126
127
128@counting_cogent.dp.operator("increment")
129class IncrementOperator(Operator[CountingState]):
130 """
131 Increments the counter
132 """
133
134 def can_perform(self, _state: CountingState, io: IOContainer) -> bool:
135 return cast(bool, io.e.is_initialized)
136
137 def perform(self, state: CountingState, _io: IOContainer) -> None:
138 assert state.counter is not None # for type checking
139
140 new_val = state.counter + 1
141 print(f"Increment: {state.counter} -> {new_val}")
142 state.counter = new_val
143
144
145# ===
146
147examples = [1, 2, 5, 11, 101]
148
149for starting_val in examples:
150 print(f"== {starting_val=} ==")
151
152 # when running the agent, kwarg passed to the
153 # agent appear as io.a.kwarg
154 output = cast(int, counting_cogent(counter_start=starting_val).dp.state.counter)
155
156 # print(counting_cogent.dp) # uncomment to see lots of debug details about the DP
157
158 print()
159 print(f"The first perfect square ≥ {starting_val} is {output} ({sqrt(output)=}).")
160 print()
Detailed Run Summary¶
Let’s focus on line 142, assuming starting_val is 2…
The value
2is accessible via thecounting_startargument (@io.a.counter_start)DP reinit (
state.counter->None); DP run!DP Cycle 1
Elaboration: the union is taken of all the key-value pairs produced across all elaborators (which is just one in this example), yielding
{'is_initialized': False, 'is_perfect': False, 'as_str': 'None'}TerminationCheck: if any check returns
True(including selection of aterminaloperator, by default), the DP stops; but not yet (sincefound_perfectfinds thatio.e.is_perfectisFalse)Propose: the operators are mutually exclusive (by design) and so
initis proposed as the only actionRank: only one action, so
initis selectedApply:
initchangesstate.counter->2
Cycle 2
Elaboration:
io.e->{'is_initialized': True, 'is_perfect': False, 'as_str': '2'}TerminationCheck:
False(still going!)Propose: only
incrementis proposedRank: only one action, so
incrementis selectedApply:
incrementchangesstate.counter->3
Cycle 3
Elaboration:
io.e->{'is_initialized': True, 'is_perfect': False, 'as_str': '3'}TerminationCheck:
False(still going!)Propose: only
incrementis proposedRank: only one action, so
incrementis selectedApply:
incrementchangesstate.counter->4
Cycle 4
Elaboration:
io.e->{'is_initialized': True, 'is_perfect': True, 'as_str': '4'}TerminationCheck:
True(done!)
DP run complete; Cogent gate check (done!)
Arguments (
counting_start) removed