File size: 9,102 Bytes
fe643f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fa1e750
fe643f6
 
 
 
 
 
 
fa1e750
fe643f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fa1e750
fe643f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fa1e750
fe643f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45d9b08
 
fe643f6
45d9b08
fe643f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
feb47ff
fe643f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
import marimo

__generated_with = "0.11.9"
app = marimo.App()


@app.cell(hide_code=True)
def _():
    import marimo as mo
    import synalinks

    synalinks.backend.clear_session()
    return mo, synalinks


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        r"""
        # Your first programs

        The main concept of Synalinks, is that an application (we call it a `Program`)
        is a computation graph with JSON data (called `JsonDataModel`) as edges and
        `Operation`s as nodes. What set apart Synalinks from other similar frameworks
        like DSPy or AdalFlow is that we focus on graph-based systems but also that
        it allow users to declare the computation graph using a Functional API inherited
        from [Keras](https://keras.io/).

        About modules, similar to layers in deep learning applications, modules are
        composable blocks that you can assemble in multiple ways. Providing a modular
        and composable architecture to experiment and unlock creativity.

        Note that each `Program` is also a `Module`! Allowing you to encapsulate them
        as you want.

        Many people think that what enabled the Deep Learning revolution was compute
        and data, but in reality, frameworks also played a pivotal role as they enabled
        researchers and engineers to create complex architectures without having to 
        re-implement everything from scatch.
        """
    )
    return


@app.cell
def _(synalinks):
    # Now we can define the data models that we are going to use in the notebook.
    # Note that Synalinks use Pydantic as default data backend, which is compatible with FastAPI and structured output.

    class Query(synalinks.DataModel):
        query: str = synalinks.Field(
            description="The user query",
        )

    class AnswerWithThinking(synalinks.DataModel):
        thinking: str = synalinks.Field(
            description="Your step by step thinking process",
        )
        answer: str = synalinks.Field(
            description="The correct answer",
        )

    return AnswerWithThinking, Query


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        r"""
        ## Functional API

        You can program your application using 3 different ways, let's start with the
        Functional way.

        In this case, you start from `Input` and you chain modules calls to specify the
        programs's structure, and finally, you create your program from inputs and outputs:
        """
    )
    return


@app.cell
async def _(AnswerWithThinking, Query, synalinks):
    
    language_model = synalinks.LanguageModel(
        model="openai/gpt-4o-mini",
    )

    _x0 = synalinks.Input(data_model=Query)
    _x1 = await synalinks.Generator(
        data_model=AnswerWithThinking,
        language_model=language_model,
    )(_x0)

    program = synalinks.Program(
        inputs=_x0,
        outputs=_x1,
        name="chain_of_thought",
        description="Useful to answer in a step by step manner.",
    )
    return language_model, program


@app.cell
def _(program):
    # You can print a summary of your program in a table format
    # which is really useful to have a quick overview of your application

    program.summary()
    return


@app.cell
def _(mo, program, synalinks):
    # Or plot your program in a graph format

    synalinks.utils.plot_program(
        program,
        show_module_names=True,
        show_trainable=True,
        show_schemas=True,
    )
    return


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        r"""
        ## Subclassing the `Program` class

        Now let's try to program it using another method, subclassing the `Program`
        class.

        In that case, you should define your modules in `__init__()` and you should
        implement the program's structure in `call()`.
        """
    )
    return


@app.cell
def _(AnswerWithThinking, language_model, synalinks):
    
    class ChainOfThought(synalinks.Program):
        """Useful to answer in a step by step manner.

        The first line of the docstring is provided as description for the program
        if not provided in the `super().__init__()`. In a similar way the name is
        automatically infered based on the class name if not provided.
        """

        def __init__(self, language_model=None):
            super().__init__()
            self.answer = synalinks.Generator(
                data_model=AnswerWithThinking, language_model=language_model
            )

        async def call(self, inputs, training=False):
            x = await self.answer(inputs)
            return x

        def get_config(self):
            config = {
                "name": self.name,
                "description": self.description,
                "trainable": self.trainable,
            }
            language_model_config = {
                "language_model": synalinks.saving.serialize_synalinks_object(
                    self.language_model
                )
            }
            return {**config, **language_model_config}

        @classmethod
        def from_config(cls, config):
            language_model = synalinks.saving.deserialize_synalinks_object(
                config.pop("language_model")
            )
            return cls(language_model=language_model, **config)

    program_1 = ChainOfThought(language_model=language_model)
    return ChainOfThought, program_1


@app.cell
def _(program_1):
    program_1.summary()
    return


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        r"""
        Note that the program isn't actually built, this behavior is intended its 
        means that it can accept any king of input, making the program truly 
        generalizable. Now we can explore the last way of programming as well as 
        illustrate one of the key feature of Synalinks, composability.

        ## Using `Sequential` program

        In addition to the other ways of programming, `Sequential` is a special
        case of programs where the program is purely a stack of single-input, 
        single-output modules.

        In this example, we are going to re-use the `ChainOfThought` program that 
        we defined previously, illustrating the modularity of the framework.
        """
    )
    return


@app.cell
def _(ChainOfThought, Query, language_model, synalinks):
    program_2 = synalinks.Sequential(
        [
            synalinks.Input(data_model=Query),
            ChainOfThought(language_model=language_model),
        ],
        name="chain_of_thought",
        description="Useful to answer in a step by step manner.",
    )
    program_2.summary()
    return (program_2,)


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        r"""
        ## Running your programs
        
        In order to run your program, you just have to call it with the input data model
        as argument.
        """
    )
    return


@app.cell(hide_code=True)
def _(mo):
    openai_api_key = mo.ui.text_area(placeholder="Your OpenAI API key...").form()
    openai_api_key
    return


@app.cell(hide_code=True)
def _(mo, openai_api_key, litellm):
    import litellm
    mo.stop(not openai_api_key.value)
    litellm.openai_key = openai_api_key.value
    return


@app.cell(hide_code=True)
def _(mo):
    run_button = mo.ui.run_button(label="Run program")
    run_button.center()
    return run_button


@app.cell
async def _(Query, program_2):
    mo.stop(not openai_api_key.value, mo.md("Provide your OpenAI API key"))
    mo.stop(not run_button.value, mo.md("Click on the run button above"))
    
    result = await program_2(
        Query(query="What are the key aspects of human cognition?"),
    )
    
    print(result.prettify_json())
    
    return (result,)


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        r"""
        ## Conclusion
        
        Congratulations! You've successfully explored the fundamental concepts of programming
        applications using Synalinks. By understanding and implementing the Functional API,
        subclassing the `Program` class, and using `Sequential` programs, you've gained a
        solid foundation in creating modular and composable applications.
        
        Now that we know how to program applications, you can learn how to control
        the data flow in the next notebook.
        
        ### Key Takeaways
        
        - **Functional API**: Allows you to chain modules to define the program's structure, 
            providing a clear and intuitive way to build applications.
        - **Subclassing**: Offers flexibility and control by defining modules and implementing
            the program's structure from scratch within a class.
        - **Sequential Programs**: Simplifies the creation of linear workflows, making it easy
            to stack single-input, single-output modules.
        - **Modularity and Composability**: Enables the reuse of components, fostering creativity
            and efficiency in application development.
        """
    )
    return


if __name__ == "__main__":
    app.run()