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 DP Elaboration phase)

    • This will perform the math to identify a perfect square (independent of other logic)

    • Also, create_elaborator: a convenience function for creating an Elaborator based upon kwargs that associate feature_name with a function to compute the associated value

  • Termination Check: a function that (during the DP TerminationCheck phase) determines if the DP should end

    • Unlike 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.name

    • In this case, the starting value

  • Logging: using the standard Python logging library

    • Debug output: using the stringify function 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…

  1. The value 2 is accessible via the counting_start argument (@ io.a.counter_start)

  2. DP reinit (state.counter -> None); DP run!

    1. DP Cycle 1

      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'}

      2. TerminationCheck: if any check returns True (including selection of a terminal operator, by default), the DP stops; but not yet (since found_perfect finds that io.e.is_perfect is False)

      3. Propose: the operators are mutually exclusive (by design) and so init is proposed as the only action

      4. Rank: only one action, so init is selected

      5. Apply: init changes state.counter -> 2

    2. Cycle 2

      1. Elaboration: io.e -> {'is_initialized': True, 'is_perfect': False, 'as_str': '2'}

      2. TerminationCheck: False (still going!)

      3. Propose: only increment is proposed

      4. Rank: only one action, so increment is selected

      5. Apply: increment changes state.counter -> 3

    3. Cycle 3

      1. Elaboration: io.e -> {'is_initialized': True, 'is_perfect': False, 'as_str': '3'}

      2. TerminationCheck: False (still going!)

      3. Propose: only increment is proposed

      4. Rank: only one action, so increment is selected

      5. Apply: increment changes state.counter -> 4

    4. Cycle 4

      1. Elaboration: io.e -> {'is_initialized': True, 'is_perfect': True, 'as_str': '4'}

      2. TerminationCheck: True (done!)

  3. DP run complete; Cogent gate check (done!)

  4. Arguments (counting_start) removed