appkit-assistant 0.7.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,299 @@
1
+ import reflex as rx
2
+
3
+ import appkit_mantine as mn
4
+ from appkit_assistant.backend.models import (
5
+ Message,
6
+ MessageType,
7
+ )
8
+ from appkit_assistant.state.thread_state import (
9
+ Thinking,
10
+ ThinkingStatus,
11
+ ThinkingType,
12
+ ThreadState,
13
+ )
14
+ from appkit_ui.components.collabsible import collabsible
15
+
16
+ message_styles = {
17
+ "spacing": "4",
18
+ "width": "100%",
19
+ "max_width": "880px",
20
+ "margin_top": "24px",
21
+ "margin_left": "auto",
22
+ "margin_right": "auto",
23
+ }
24
+
25
+
26
+ class MessageComponent:
27
+ @staticmethod
28
+ def human_message(message: str) -> rx.Component:
29
+ return rx.hstack(
30
+ rx.spacer(),
31
+ rx.box(
32
+ rx.text(
33
+ message,
34
+ padding="0.5em",
35
+ border_radius="10px",
36
+ white_space="pre-line",
37
+ ),
38
+ padding="4px",
39
+ max_width="80%",
40
+ margin_top="24px",
41
+ # margin_right="14px",
42
+ background_color=rx.color_mode_cond(
43
+ light=rx.color("accent", 3),
44
+ dark=rx.color("accent", 3),
45
+ ),
46
+ border_radius="9px",
47
+ ),
48
+ style=message_styles,
49
+ )
50
+
51
+ @staticmethod
52
+ def assistant_message(message: Message) -> rx.Component:
53
+ """Display an assistant message with thinking content when items exist."""
54
+
55
+ # Show thinking content only for the last assistant message
56
+ should_show_thinking = (
57
+ message.text == ThreadState.last_assistant_message_text
58
+ ) & ThreadState.has_thinking_content
59
+
60
+ # Main content area with all components
61
+ content_area = rx.vstack(
62
+ # Always rendered with conditional styling for smooth animations
63
+ collabsible(
64
+ rx.scroll_area(
65
+ rx.foreach(
66
+ ThreadState.thinking_items,
67
+ lambda item: ToolCallComponent.render(item),
68
+ ),
69
+ spacing="3",
70
+ max_height="180px",
71
+ padding="9px 12px",
72
+ width="100%",
73
+ scrollbars="vertical",
74
+ ),
75
+ title="Denkprozess & Werkzeuge",
76
+ info_text=(
77
+ f"{ThreadState.unique_reasoning_sessions.length()} "
78
+ f"Nachdenken, "
79
+ f"{ThreadState.unique_tool_calls.length()} Werkzeuge"
80
+ ),
81
+ show_condition=should_show_thinking,
82
+ expanded=ThreadState.thinking_expanded,
83
+ on_toggle=ThreadState.toggle_thinking_expanded,
84
+ ),
85
+ # Main message content
86
+ rx.cond(
87
+ message.text == "",
88
+ rx.hstack(
89
+ rx.text(
90
+ rx.cond(
91
+ ThreadState.current_activity != "",
92
+ ThreadState.current_activity,
93
+ "Denke nach",
94
+ ),
95
+ color=rx.color("gray", 8),
96
+ margin_right="9px",
97
+ ),
98
+ rx.hstack(
99
+ rx.el.span(""),
100
+ rx.el.span(""),
101
+ rx.el.span(""),
102
+ rx.el.span(""),
103
+ ),
104
+ class_name="loading",
105
+ height="40px",
106
+ color=rx.color("gray", 8),
107
+ background_color=rx.color("gray", 2),
108
+ padding="0.5em",
109
+ border_radius="9px",
110
+ margin_top="16px",
111
+ padding_right="18px",
112
+ ),
113
+ # Actual message content
114
+ mn.markdown_preview(
115
+ source=message.text,
116
+ enable_mermaid=message.done,
117
+ enable_katex=message.done,
118
+ security_level="standard",
119
+ padding="0.5em",
120
+ border_radius="9px",
121
+ max_width="90%",
122
+ class_name="markdown",
123
+ ),
124
+ # rx.markdown(
125
+ # message.text,
126
+ # padding="0.5em",
127
+ # border_radius="9px",
128
+ # max_width="90%",
129
+ # class_name="markdown",
130
+ # ),
131
+ ),
132
+ spacing="3",
133
+ width="100%",
134
+ )
135
+
136
+ return rx.hstack(
137
+ rx.avatar(
138
+ fallback="AI",
139
+ size="3",
140
+ variant="soft",
141
+ radius="full",
142
+ margin_top="16px",
143
+ ),
144
+ content_area,
145
+ style=message_styles,
146
+ )
147
+
148
+ @staticmethod
149
+ def info_message(message: str) -> rx.Component:
150
+ return rx.hstack(
151
+ rx.avatar(
152
+ fallback="AI",
153
+ size="3",
154
+ variant="soft",
155
+ radius="full",
156
+ margin_top="16px",
157
+ ),
158
+ rx.callout(
159
+ message,
160
+ icon="info",
161
+ max_width="90%",
162
+ size="1",
163
+ padding="0.5em",
164
+ border_radius="9px",
165
+ margin_top="18px",
166
+ ),
167
+ style=message_styles,
168
+ )
169
+
170
+ @staticmethod
171
+ def render_message(
172
+ message: Message,
173
+ ) -> rx.Component:
174
+ """Render message with optional enhanced chunk-based components."""
175
+ return rx.fragment(
176
+ rx.match(
177
+ message.type,
178
+ (
179
+ MessageType.HUMAN,
180
+ MessageComponent.human_message(message.text),
181
+ ),
182
+ (
183
+ MessageType.ASSISTANT,
184
+ MessageComponent.assistant_message(message),
185
+ ),
186
+ MessageComponent.info_message(message.text),
187
+ )
188
+ )
189
+
190
+
191
+ class ToolCallComponent:
192
+ """Component for displaying individual tool calls with green styling."""
193
+
194
+ @staticmethod
195
+ def render(tool_item: Thinking) -> rx.Component:
196
+ return rx.cond(
197
+ tool_item.type == ThinkingType.REASONING,
198
+ ToolCallComponent._render_reasoning(tool_item),
199
+ ToolCallComponent._render_tool_call(tool_item),
200
+ )
201
+
202
+ @staticmethod
203
+ def _render_reasoning(item: Thinking) -> rx.Component:
204
+ return rx.vstack(
205
+ rx.text(item.text, size="1"),
206
+ border_left=f"3px solid {rx.color('gray', 4)}",
207
+ padding="3px 6px",
208
+ margin_bottom="9px",
209
+ )
210
+
211
+ @staticmethod
212
+ def _render_tool_call(item: Thinking) -> rx.Component:
213
+ return rx.vstack(
214
+ rx.hstack(
215
+ rx.icon("wrench", size=14, color=rx.color("green", 8)),
216
+ rx.text(
217
+ f"Werkzeug: {item.tool_name}",
218
+ size="1",
219
+ font_weight="bold",
220
+ color=rx.color("blue", 9),
221
+ ),
222
+ rx.spacer(),
223
+ rx.text(
224
+ item.id,
225
+ size="1",
226
+ color=rx.color("gray", 6),
227
+ ),
228
+ spacing="1",
229
+ margin_bottom="3px",
230
+ width="100%",
231
+ ),
232
+ rx.cond(
233
+ item.text,
234
+ rx.vstack(
235
+ rx.text(
236
+ item.text,
237
+ size="1",
238
+ color=rx.color("gray", 10),
239
+ ),
240
+ align="start",
241
+ width="100%",
242
+ ),
243
+ ),
244
+ rx.cond(
245
+ item.parameters,
246
+ rx.vstack(
247
+ rx.text(
248
+ item.parameters,
249
+ size="1",
250
+ color=rx.color("blue", 9),
251
+ white_space="pre-wrap",
252
+ ),
253
+ align="start",
254
+ width="100%",
255
+ spacing="1",
256
+ ),
257
+ ),
258
+ rx.cond(
259
+ item.status == ThinkingStatus.COMPLETED,
260
+ rx.scroll_area(
261
+ rx.text(
262
+ item.result,
263
+ size="1",
264
+ color=rx.color("gray", 8),
265
+ ),
266
+ max_height="60px",
267
+ width="95%",
268
+ scrollbars="vertical",
269
+ ),
270
+ ),
271
+ rx.cond(
272
+ item.status == ThinkingStatus.ERROR,
273
+ rx.vstack(
274
+ rx.hstack(
275
+ rx.icon("shield-alert", size=14, color=rx.color("red", 10)),
276
+ rx.text(
277
+ "Fehler",
278
+ size="1",
279
+ font_weight="bold",
280
+ color=rx.color("red", 10),
281
+ ),
282
+ spacing="1",
283
+ ),
284
+ rx.text(
285
+ item.error,
286
+ size="1",
287
+ color=rx.color("red", 9),
288
+ ),
289
+ align="start",
290
+ width="100%",
291
+ spacing="1",
292
+ ),
293
+ ),
294
+ padding="3px 6px",
295
+ border_left=f"3px solid {rx.color('gray', 5)}",
296
+ margin_bottom="9px",
297
+ width="100%",
298
+ spacing="2",
299
+ )
@@ -0,0 +1,252 @@
1
+ import logging
2
+ from collections.abc import Callable
3
+
4
+ import reflex as rx
5
+
6
+ import appkit_mantine as mn
7
+ from appkit_assistant.components import composer
8
+ from appkit_assistant.components.message import MessageComponent
9
+ from appkit_assistant.components.threadlist import ThreadList
10
+ from appkit_assistant.state.thread_state import (
11
+ Message,
12
+ MessageType,
13
+ ThreadListState,
14
+ ThreadState,
15
+ )
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class Assistant:
21
+ @staticmethod
22
+ def suggestion(
23
+ prompt: str,
24
+ icon: str | None = None,
25
+ update_prompt: Callable | None = None,
26
+ **props,
27
+ ) -> rx.Component:
28
+ """Component to display a suggestion."""
29
+
30
+ on_click_handler = update_prompt(prompt) if update_prompt else None
31
+
32
+ return rx.button(
33
+ rx.cond(icon, rx.icon(icon), None),
34
+ prompt,
35
+ size="2",
36
+ variant="soft",
37
+ radius="large",
38
+ on_click=on_click_handler,
39
+ **props,
40
+ )
41
+
42
+ @staticmethod
43
+ def empty(
44
+ welcome_message: str,
45
+ **props,
46
+ ) -> rx.Component:
47
+ """Component to display when there are no messages."""
48
+ return rx.vstack(
49
+ rx.text(welcome_message, size="8", margin_bottom="0.5em"),
50
+ rx.cond(
51
+ ThreadState.has_suggestions,
52
+ rx.flex(
53
+ rx.foreach(
54
+ ThreadState.suggestions,
55
+ lambda suggestion: Assistant.suggestion(
56
+ prompt=suggestion.prompt,
57
+ icon=suggestion.icon,
58
+ update_prompt=ThreadState.update_prompt,
59
+ ),
60
+ ),
61
+ spacing="4",
62
+ width="100%",
63
+ direction="row",
64
+ wrap="wrap",
65
+ ),
66
+ None,
67
+ ),
68
+ **props,
69
+ )
70
+
71
+ @staticmethod
72
+ def messages(
73
+ **props,
74
+ ) -> rx.Component:
75
+ """Component to display messages in the thread."""
76
+
77
+ if ThreadState.messages is None:
78
+ messages = [Message(text="👋 Hi!", type=MessageType.ASSISTANT)]
79
+ else:
80
+ messages = ThreadState.messages
81
+
82
+ return rx.fragment(
83
+ mn.scroll_area.stateful(
84
+ rx.foreach(
85
+ messages,
86
+ lambda message: MessageComponent.render_message(message),
87
+ ),
88
+ rx.spacer(
89
+ id="scroll-anchor",
90
+ display="hidden",
91
+ min_height="44px",
92
+ wrap="nowrap",
93
+ ),
94
+ type="hover",
95
+ autoscroll=True,
96
+ show_controls=True,
97
+ controls="both",
98
+ top_button_text="↑",
99
+ bottom_button_text="↓",
100
+ offset_scrollbars=False,
101
+ scrollbars="y",
102
+ scrollbar_size="6px",
103
+ height="100%",
104
+ min_height="0",
105
+ **props,
106
+ ),
107
+ )
108
+
109
+ @staticmethod
110
+ def composer(
111
+ with_attachments: bool = False,
112
+ with_model_chooser: bool = False,
113
+ with_tools: bool = False,
114
+ with_clear: bool = True,
115
+ **props,
116
+ ) -> rx.Component:
117
+ return composer(
118
+ composer.input(),
119
+ rx.hstack(
120
+ rx.hstack(
121
+ composer.choose_model(show=with_model_chooser),
122
+ ),
123
+ rx.hstack(
124
+ composer.tools(
125
+ show=with_tools and ThreadState.current_model_supports_tools
126
+ ),
127
+ composer.add_attachment(show=with_attachments),
128
+ composer.clear(show=with_clear),
129
+ composer.submit(),
130
+ width="100%",
131
+ justify="end",
132
+ align="center",
133
+ spacing="4",
134
+ ),
135
+ padding="0 12px 12px 12px",
136
+ width="100%",
137
+ align="center",
138
+ ),
139
+ on_submit=ThreadState.submit_message,
140
+ **props,
141
+ )
142
+
143
+ @staticmethod
144
+ def thread(
145
+ welcome_message: str = "",
146
+ with_attachments: bool = False,
147
+ with_clear: bool = True,
148
+ with_model_chooser: bool = True,
149
+ with_scroll_to_bottom: bool = False,
150
+ with_thread_list: bool = False,
151
+ with_tools: bool = False,
152
+ **props,
153
+ ) -> rx.Component:
154
+ # Note: avoid mutating state during component tree building
155
+ # Use ThreadState.set_suggestions() event handler to update suggestions
156
+ # if suggestions is not None:
157
+ # ThreadState.set_suggestions(suggestions)
158
+
159
+ if with_thread_list:
160
+ ThreadState.with_thread_list = with_thread_list
161
+
162
+ return rx.flex(
163
+ rx.cond(
164
+ ThreadState.messages,
165
+ Assistant.messages(
166
+ with_scroll_to_bottom=with_scroll_to_bottom,
167
+ width="100%",
168
+ margin_bottom="-1em",
169
+ flex_grow=1,
170
+ justify_content="start",
171
+ ),
172
+ Assistant.empty(
173
+ welcome_message=welcome_message,
174
+ width="100%",
175
+ max_width="880px",
176
+ margin_left="auto",
177
+ margin_right="auto",
178
+ margin_bottom="2em",
179
+ flex_grow=1,
180
+ justify_content="flex-end",
181
+ ),
182
+ ),
183
+ Assistant.composer(
184
+ with_attachments=with_attachments,
185
+ with_tools=with_tools,
186
+ with_model_chooser=with_model_chooser,
187
+ with_clear=with_clear,
188
+ # styling
189
+ border=rx.color_mode_cond(
190
+ light=f"1px solid {rx.color('gray', 9)}",
191
+ dark=f"1px solid {rx.color('white', 7, alpha=True)}",
192
+ ),
193
+ box_shadow=rx.color_mode_cond(
194
+ light="0 1px 10px -0.5px rgba(0, 0, 0, 0.1)",
195
+ dark="0 1px 10px -0.5px rgba(0.8, 0.8, 0.8, 0.1)",
196
+ ),
197
+ border_radius="10px",
198
+ background_color=rx.color_mode_cond(
199
+ light=rx.color("white", 9, alpha=True),
200
+ dark=rx.color("white", 2, alpha=False),
201
+ ),
202
+ width="100%",
203
+ max_width="880px",
204
+ margin_left="auto",
205
+ margin_right="auto",
206
+ margin_top="1em",
207
+ spacing="0",
208
+ flex_shrink=0,
209
+ z_index=1000,
210
+ on_mount=ThreadState.load_available_mcp_servers,
211
+ ),
212
+ **props,
213
+ )
214
+
215
+ @staticmethod
216
+ def thread_list(
217
+ *items,
218
+ with_footer: bool = False,
219
+ default_model: str | None = None,
220
+ **props,
221
+ ) -> rx.Component:
222
+ if default_model:
223
+ ThreadListState.default_model = default_model
224
+
225
+ return rx.flex(
226
+ rx.flex(
227
+ ThreadList.header(
228
+ title="Neuer Chat",
229
+ margin_bottom="1.5em",
230
+ flex_shrink=0,
231
+ ),
232
+ ThreadList.list(
233
+ flex_grow=1,
234
+ min_height="60px",
235
+ ),
236
+ rx.cond(
237
+ with_footer,
238
+ ThreadList.footer(
239
+ *items,
240
+ flex_shrink=0,
241
+ min_height="48px",
242
+ ),
243
+ None,
244
+ ),
245
+ flex_direction=["column"],
246
+ width="100%",
247
+ height="100%",
248
+ overflow="hidden",
249
+ ),
250
+ overflow="hidden",
251
+ **props,
252
+ )
@@ -0,0 +1,134 @@
1
+ import reflex as rx
2
+
3
+ from appkit_assistant.state.thread_state import ThreadListState, ThreadModel
4
+
5
+
6
+ class ThreadList:
7
+ @staticmethod
8
+ def header(title: str = "Neuer Chat", **props) -> rx.Component:
9
+ """Header component for the thread list."""
10
+ return rx.flex(
11
+ rx.tooltip(
12
+ rx.button(
13
+ rx.text(title),
14
+ size="2",
15
+ margin_right="28px",
16
+ on_click=ThreadListState.create_thread(),
17
+ width="95%",
18
+ ),
19
+ content="Neuen Chat starten",
20
+ ),
21
+ direction="row",
22
+ align="center",
23
+ margin_top="9px",
24
+ **props,
25
+ )
26
+
27
+ @staticmethod
28
+ def footer(*items, **props) -> rx.Component:
29
+ """Footer component for the thread list."""
30
+ return rx.flex(
31
+ *items,
32
+ **props,
33
+ )
34
+
35
+ @staticmethod
36
+ def thread_list_item(thread: ThreadModel) -> rx.Component:
37
+ return rx.flex(
38
+ rx.text(
39
+ thread.title,
40
+ size="2",
41
+ white_space="nowrap",
42
+ overflow="hidden",
43
+ text_overflow="ellipsis",
44
+ flex_grow="1",
45
+ width="100px",
46
+ min_width="0",
47
+ title=thread.title,
48
+ ),
49
+ rx.tooltip(
50
+ rx.button(
51
+ rx.icon(
52
+ "trash",
53
+ size=13,
54
+ stroke_width=1.5,
55
+ ),
56
+ variant="ghost",
57
+ size="1",
58
+ margin_left="0px",
59
+ margin_right="0px",
60
+ color_scheme="gray",
61
+ on_click=ThreadListState.delete_thread(thread.thread_id),
62
+ ),
63
+ content="Chat löschen",
64
+ flex_shrink=0,
65
+ ),
66
+ on_click=ThreadListState.select_thread(thread.thread_id),
67
+ flex_direction=["row"],
68
+ margin_right="10px",
69
+ margin_bottom="8px",
70
+ padding="6px",
71
+ align="center",
72
+ border_radius="8px",
73
+ background_color=rx.cond(
74
+ thread.active,
75
+ rx.color("accent", 3),
76
+ rx.color("gray", 3),
77
+ ),
78
+ border=rx.cond(
79
+ thread.active,
80
+ f"1px solid {rx.color('gray', 5)}",
81
+ "0",
82
+ ),
83
+ style={
84
+ "_hover": {
85
+ "cursor": "pointer",
86
+ "background_color": rx.cond(
87
+ thread.active,
88
+ rx.color("accent", 4),
89
+ rx.color("gray", 6),
90
+ ),
91
+ "color": rx.cond(
92
+ thread.active,
93
+ rx.color("black", 9),
94
+ rx.color("white", 9),
95
+ ),
96
+ "opacity": "1",
97
+ },
98
+ "opacity": rx.cond(
99
+ thread.active,
100
+ "1",
101
+ "0.95",
102
+ ),
103
+ },
104
+ )
105
+
106
+ @staticmethod
107
+ def list(**props) -> rx.Component:
108
+ """List component for displaying threads."""
109
+ return rx.scroll_area(
110
+ rx.cond(
111
+ ThreadListState.has_threads,
112
+ rx.foreach(
113
+ ThreadListState.threads,
114
+ ThreadList.thread_list_item,
115
+ ),
116
+ rx.text(
117
+ "Keine Chats vorhanden.",
118
+ size="2",
119
+ white_space="nowrap",
120
+ overflow="hidden",
121
+ text_overflow="ellipsis",
122
+ flex_grow="1",
123
+ min_width="0",
124
+ margin_right="10px",
125
+ margin_bottom="8px",
126
+ padding="6px",
127
+ align="center",
128
+ ),
129
+ ),
130
+ scrollbars="vertical",
131
+ padding_right="3px",
132
+ type="auto",
133
+ **props,
134
+ )