appkit-assistant 0.16.2__py3-none-any.whl → 0.17.0__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.
@@ -18,6 +18,7 @@ from typing import Any
18
18
 
19
19
  import reflex as rx
20
20
 
21
+ from appkit_assistant.backend import file_manager
21
22
  from appkit_assistant.backend.model_manager import ModelManager
22
23
  from appkit_assistant.backend.models import (
23
24
  AIModel,
@@ -31,10 +32,11 @@ from appkit_assistant.backend.models import (
31
32
  ThinkingType,
32
33
  ThreadModel,
33
34
  ThreadStatus,
35
+ UploadedFile,
34
36
  )
35
37
  from appkit_assistant.backend.repositories import mcp_server_repo
38
+ from appkit_assistant.backend.response_accumulator import ResponseAccumulator
36
39
  from appkit_assistant.backend.services.thread_service import ThreadService
37
- from appkit_assistant.logic.response_accumulator import ResponseAccumulator
38
40
  from appkit_assistant.state.thread_list_state import ThreadListState
39
41
  from appkit_commons.database.session import get_asyncdb_session
40
42
  from appkit_user.authentication.states import UserSession
@@ -69,6 +71,9 @@ class ThreadState(rx.State):
69
71
  thinking_expanded: bool = False
70
72
  current_activity: str = ""
71
73
 
74
+ # File upload state
75
+ uploaded_files: list[UploadedFile] = []
76
+
72
77
  # Internal logic helper (not reactive)
73
78
  @property
74
79
  def _thread_service(self) -> ThreadService:
@@ -97,6 +102,7 @@ class ThreadState(rx.State):
97
102
  _initialized: bool = False
98
103
  _current_user_id: str = ""
99
104
  _skip_user_message: bool = False # Skip adding user message (for OAuth resend)
105
+ _pending_file_cleanup: list[str] = [] # Files to delete after processing
100
106
 
101
107
  # -------------------------------------------------------------------------
102
108
  # Computed properties
@@ -135,6 +141,14 @@ class ThreadState(rx.State):
135
141
  model = ModelManager().get_model(self.selected_model)
136
142
  return model.supports_tools if model else False
137
143
 
144
+ @rx.var
145
+ def selected_model_supports_attachments(self) -> bool:
146
+ """Check if the currently selected model supports attachments."""
147
+ if not self.selected_model:
148
+ return False
149
+ model = ModelManager().get_model(self.selected_model)
150
+ return model.supports_attachments if model else False
151
+
138
152
  @rx.var
139
153
  def get_unique_reasoning_sessions(self) -> list[str]:
140
154
  """Get unique reasoning session IDs."""
@@ -432,6 +446,65 @@ class ThreadState(rx.State):
432
446
  self.thinking_items = []
433
447
  self.image_chunks = []
434
448
  self.show_thinking = False
449
+ self._clear_uploaded_files()
450
+
451
+ # -------------------------------------------------------------------------
452
+ # File upload handling
453
+ # -------------------------------------------------------------------------
454
+
455
+ @rx.event
456
+ async def handle_upload(self, files: list[rx.UploadFile]) -> None:
457
+ """Handle uploaded files from the browser.
458
+
459
+ Moves files to user-specific directory and adds them to state.
460
+ """
461
+ user_session: UserSession = await self.get_state(UserSession)
462
+ user_id = user_session.user.user_id if user_session.user else "anonymous"
463
+
464
+ for upload_file in files:
465
+ try:
466
+ # Save uploaded file to disk first
467
+ upload_data = await upload_file.read()
468
+ temp_path = rx.get_upload_dir() / upload_file.filename
469
+ temp_path.parent.mkdir(parents=True, exist_ok=True)
470
+ temp_path.write_bytes(upload_data)
471
+
472
+ # Move to user directory
473
+ final_path = file_manager.move_to_user_directory(
474
+ str(temp_path), str(user_id)
475
+ )
476
+ file_size = file_manager.get_file_size(final_path)
477
+
478
+ uploaded = UploadedFile(
479
+ filename=upload_file.filename,
480
+ file_path=final_path,
481
+ size=file_size,
482
+ )
483
+ self.uploaded_files = [*self.uploaded_files, uploaded]
484
+ logger.info("Uploaded file: %s", upload_file.filename)
485
+ except Exception as e:
486
+ logger.error("Failed to upload file %s: %s", upload_file.filename, e)
487
+
488
+ @rx.event
489
+ def remove_file_from_prompt(self, file_path: str) -> None:
490
+ """Remove an uploaded file from the prompt."""
491
+ # Delete the file from disk
492
+ file_manager.cleanup_uploaded_files([file_path])
493
+ # Remove from state
494
+ self.uploaded_files = [
495
+ f for f in self.uploaded_files if f.file_path != file_path
496
+ ]
497
+ logger.debug("Removed uploaded file: %s", file_path)
498
+
499
+ def _clear_uploaded_files(self) -> None:
500
+ """Clear all uploaded files from state and disk."""
501
+ if not self.uploaded_files:
502
+ return
503
+ count = len(self.uploaded_files)
504
+ file_paths = [f.file_path for f in self.uploaded_files]
505
+ file_manager.cleanup_uploaded_files(file_paths)
506
+ self.uploaded_files = []
507
+ logger.debug("Cleared %d uploaded files", count)
435
508
 
436
509
  # -------------------------------------------------------------------------
437
510
  # Message processing
@@ -458,7 +531,7 @@ class ThreadState(rx.State):
458
531
  start = await self._begin_message_processing()
459
532
  if not start:
460
533
  return
461
- current_prompt, selected_model, mcp_servers, is_new_thread = start
534
+ current_prompt, selected_model, mcp_servers, file_paths, is_new_thread = start
462
535
 
463
536
  processor = ModelManager().get_processor_for_model(selected_model)
464
537
  if not processor:
@@ -475,11 +548,16 @@ class ThreadState(rx.State):
475
548
  accumulator = ResponseAccumulator()
476
549
  accumulator.attach_messages_ref(self.messages)
477
550
 
551
+ # Clear uploaded files from UI and mark for cleanup after processing
552
+ self.uploaded_files = []
553
+ self._pending_file_cleanup = file_paths
554
+
478
555
  first_response_received = False
479
556
  try:
480
557
  async for chunk in processor.process(
481
558
  self.messages,
482
559
  selected_model,
560
+ files=file_paths or None,
483
561
  mcp_servers=mcp_servers,
484
562
  user_id=user_id,
485
563
  ):
@@ -506,7 +584,7 @@ class ThreadState(rx.State):
506
584
 
507
585
  async def _begin_message_processing(
508
586
  self,
509
- ) -> tuple[str, str, list[MCPServer], bool] | None:
587
+ ) -> tuple[str, str, list[MCPServer], list[str], bool] | None:
510
588
  """Prepare state for sending a message. Returns None if no-op."""
511
589
  async with self:
512
590
  current_prompt = self.prompt.strip()
@@ -523,12 +601,21 @@ class ThreadState(rx.State):
523
601
 
524
602
  is_new_thread = self._thread.state == ThreadStatus.NEW
525
603
 
604
+ # Capture file paths before clearing
605
+ file_paths = [f.file_path for f in self.uploaded_files]
606
+ # Capture filenames for message display
607
+ attachment_names = [f.filename for f in self.uploaded_files]
608
+
526
609
  # Add user message unless skipped (e.g., OAuth resend)
527
610
  if self._skip_user_message:
528
611
  self._skip_user_message = False
529
612
  else:
530
613
  self.messages.append(
531
- Message(text=current_prompt, type=MessageType.HUMAN)
614
+ Message(
615
+ text=current_prompt,
616
+ type=MessageType.HUMAN,
617
+ attachments=attachment_names,
618
+ )
532
619
  )
533
620
  # Always add assistant placeholder
534
621
  self.messages.append(Message(text="", type=MessageType.ASSISTANT))
@@ -540,7 +627,13 @@ class ThreadState(rx.State):
540
627
  return None
541
628
 
542
629
  mcp_servers = self.selected_mcp_servers
543
- return current_prompt, selected_model, mcp_servers, is_new_thread
630
+ return (
631
+ current_prompt,
632
+ selected_model,
633
+ mcp_servers,
634
+ file_paths,
635
+ is_new_thread,
636
+ )
544
637
 
545
638
  async def _stop_processing_with_error(self, error_msg: str) -> None:
546
639
  """Stop processing and show an error message."""
@@ -650,6 +743,11 @@ class ThreadState(rx.State):
650
743
  self.processing = False
651
744
  self.current_activity = ""
652
745
 
746
+ # Clean up uploaded files from disk
747
+ if self._pending_file_cleanup:
748
+ file_manager.cleanup_uploaded_files(self._pending_file_cleanup)
749
+ self._pending_file_cleanup = []
750
+
653
751
  def _handle_auth_required_from_accumulator(
654
752
  self, accumulator: ResponseAccumulator
655
753
  ) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: appkit-assistant
3
- Version: 0.16.2
3
+ Version: 0.17.0
4
4
  Summary: Add your description here
5
5
  Project-URL: Homepage, https://github.com/jenreh/appkit
6
6
  Project-URL: Documentation, https://github.com/jenreh/appkit/tree/main/docs
@@ -16,9 +16,12 @@ Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
17
  Classifier: Topic :: Software Development :: User Interfaces
18
18
  Requires-Python: >=3.13
19
+ Requires-Dist: anthropic>=0.40.0
19
20
  Requires-Dist: appkit-commons
20
21
  Requires-Dist: appkit-mantine
21
22
  Requires-Dist: appkit-ui
23
+ Requires-Dist: google-genai>=1.52.0
24
+ Requires-Dist: mcp>=1.0.0
22
25
  Requires-Dist: openai>=2.3.0
23
26
  Requires-Dist: reflex>=0.8.22
24
27
  Description-Content-Type: text/markdown
@@ -1,34 +1,39 @@
1
- appkit_assistant/configuration.py,sha256=3nBL-dEGYsvSnRDNpxtikZn4QMMkMlNbb4VqGOPolJI,346
1
+ appkit_assistant/configuration.py,sha256=Wpo3EuGWQrV0WIQnAhkj19PzgGkJoAde5ky-MA7kJwg,429
2
2
  appkit_assistant/pages.py,sha256=gDvBweUO2WjrhP1RE5AAkjL1_S-givWr3CkkGZKws_E,471
3
+ appkit_assistant/backend/file_manager.py,sha256=54SYphu6FsxbEYuMx8ohQiSAeY2gGDV1q3S6RZuNku0,3153
3
4
  appkit_assistant/backend/mcp_auth_service.py,sha256=lYQEe4yOZ48ear6dvcuOXsaOc6RClIBMsOOkV7SG5Aw,27768
4
5
  appkit_assistant/backend/model_manager.py,sha256=fmv3yP63LxDnne4vjT7IzETTI2aSxViC2FSUfHQajlk,4382
5
- appkit_assistant/backend/models.py,sha256=jNhJ6z8LKN6geJ6KTU60d10MVhWFcof6yJIVP18gtOs,7983
6
+ appkit_assistant/backend/models.py,sha256=vymgJ7npF-L-4WGXWwVUdHqLo3MAypPPaaD_SyDcAIw,8198
6
7
  appkit_assistant/backend/processor.py,sha256=LtkkC6v0idNtbuMm3Sw5J9zIjYqtNhtxYxjYefPSS9A,2135
7
8
  appkit_assistant/backend/repositories.py,sha256=R-7kYdxg4RWQrTEOU4tbcOEhJA_FlesWrt65UpItRSU,5547
9
+ appkit_assistant/backend/response_accumulator.py,sha256=dFJZiXcDn4V2POSk7Us6gQLsrUIRBMLdoOP61LwutdI,9766
8
10
  appkit_assistant/backend/system_prompt_cache.py,sha256=83OIyixeTb3HKOy3XIzPyTAE-G2JyqrfcG8xVeTS2Ls,5514
9
- appkit_assistant/backend/processors/lorem_ipsum_processor.py,sha256=j-MZhzibrtabzbGB2Pf4Xcdlr1TlTYWNRdE22LsDp9Q,4635
10
- appkit_assistant/backend/processors/openai_base.py,sha256=IQS4m375BOD_K0PBFOk4i7wL1z5MEiPFxbSmC-HBNgU,4414
11
+ appkit_assistant/backend/processors/claude_base.py,sha256=j0DhBn8EVAjW_bfCghXaEHyORO1raUNdQeemVWCKJlA,5376
12
+ appkit_assistant/backend/processors/claude_responses_processor.py,sha256=_quuN5FRsPSpQjaNIceN-Z66Fb3RnhXtcsS4gE0NrPA,32980
13
+ appkit_assistant/backend/processors/gemini_base.py,sha256=ijCa8-_xdddD6ms_pkUjCj5kHZOoQ1wfw-7AhhXu6vo,2286
14
+ appkit_assistant/backend/processors/gemini_responses_processor.py,sha256=pzbczuw7MsoABdkuJGNxlebV8POA2D72XMzLoMhj9k4,26429
15
+ appkit_assistant/backend/processors/lorem_ipsum_processor.py,sha256=WR4s1RyOi5xgWVpre04xoxoMLIAINkpdvRCkSfNuoMc,4699
16
+ appkit_assistant/backend/processors/openai_base.py,sha256=TBQCZW7RxaPMag_DX_tpLyMpZ_PZ_KiO8_sFN49OCpk,4424
11
17
  appkit_assistant/backend/processors/openai_chat_completion_processor.py,sha256=nTxouoXDU6VcQr8UhA2KiMNt60KvIwM8cH9Z8lo4dXY,4218
12
- appkit_assistant/backend/processors/openai_responses_processor.py,sha256=z4FxW7A9ysC0scyZ4gpvpoAsweYXSlMFDzGhJO5Lu2U,29094
18
+ appkit_assistant/backend/processors/openai_responses_processor.py,sha256=hysK7mlsG2t8SfltQpCvKA6MxpeIomRMGX-tSMaUshc,29492
13
19
  appkit_assistant/backend/processors/perplexity_processor.py,sha256=weHukv78MSCF_uSCKGSMpNYHsET9OB8IhpvUiMfPQ8A,3355
14
20
  appkit_assistant/backend/services/thread_service.py,sha256=LpM8ZZHt1o4MYEKzH_XPURSi3qS6p3pAQA53tOE53MU,4663
15
- appkit_assistant/components/__init__.py,sha256=5tzK5VjX9FGKK-qTUHLjr8-ohT4ykb4a-zC-I3yeRLY,916
16
- appkit_assistant/components/composer.py,sha256=F4VPxWp4P6fvTW4rQ7S-YWn0eje5c3jGsWrpC1aewss,3885
21
+ appkit_assistant/components/__init__.py,sha256=ptv0wShA4CHjgNpehlHqgoinCl-yyofjkV6rTTNIRHE,954
22
+ appkit_assistant/components/composer.py,sha256=jQNZd8Y0eQgbfC3d1AV1osRNnwfztEpXtrFp7aagXaY,7190
17
23
  appkit_assistant/components/composer_key_handler.py,sha256=KyZYyhxzFR8DH_7F_DrvTFNT6v5kG6JihlGTmCv2wv0,1028
18
24
  appkit_assistant/components/mcp_oauth.py,sha256=puLwxAhmF25BjnZMdJbKIfC6bFXK2D8LybOX0kD7Ri4,1737
19
25
  appkit_assistant/components/mcp_server_dialogs.py,sha256=afIImmhfrNyLmxDZBpCxHxvD8HKpDanIloLEC8dJgro,23444
20
26
  appkit_assistant/components/mcp_server_table.py,sha256=1dziN7hDDvE8Y3XcdIs0wUPv1H64kP9gRAEjgH9Yvzo,2323
21
- appkit_assistant/components/message.py,sha256=clqw5ISSO10NbbpD3OCfGo2CuefFl5-EIJXmlQEHncY,13554
27
+ appkit_assistant/components/message.py,sha256=xFMcrSMErHbbgZGl0rujNwhL7nRGwIp-S-a47cYwaJ4,14605
22
28
  appkit_assistant/components/system_prompt_editor.py,sha256=REl33zFmcpYRe9kxvFrBRYg40dV4L4FtVC_3ibLsmrU,2940
23
- appkit_assistant/components/thread.py,sha256=COl8PxMht4YIublIL-iOcvcJnWVpZfmLGifHC6dm7Rk,8421
29
+ appkit_assistant/components/thread.py,sha256=-KK1vcEmITR_oDGLbQhIa2WxRCVxEqel12m4haS7y9w,8461
24
30
  appkit_assistant/components/threadlist.py,sha256=1xVakSTQYi5-wgED3fTJVggeIjL_fkthehce0wKUYtM,4896
25
31
  appkit_assistant/components/tools_modal.py,sha256=12iiAVahy3j4JwjGfRlegVEa4ePhGsEu7Bq92JLn1ZI,3353
26
- appkit_assistant/logic/response_accumulator.py,sha256=85NKtIxpWVsI4rDkNAy_U34uavY8NoUEp9n2kO0ief4,8060
27
32
  appkit_assistant/state/mcp_oauth_state.py,sha256=6MofExrbOOEl_YUcUOqcSTN3h7KAaERI5IdVfXdVUVs,7669
28
33
  appkit_assistant/state/mcp_server_state.py,sha256=3AFvy53xx_eLTxw-LfJklPTgq4Ohqu4xs1QlLs-kU4U,11387
29
34
  appkit_assistant/state/system_prompt_state.py,sha256=zdnYrTnl7EszALRiodu6pcuQUd2tmtPG1eJ10j_OotI,7705
30
35
  appkit_assistant/state/thread_list_state.py,sha256=DEOR5Nklj1qfYaxSRMXCZdZRv2iq2Jb37JSg739_wL4,10250
31
- appkit_assistant/state/thread_state.py,sha256=rfbcdVTGcuC2MRnkTLP7x9fdUAHMK6NeS8cnhf621kg,30781
32
- appkit_assistant-0.16.2.dist-info/METADATA,sha256=kX6nu1t95GqH3szeSq0-2XQ2otiqLR2x_r0N_p3uDEA,9403
33
- appkit_assistant-0.16.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
34
- appkit_assistant-0.16.2.dist-info/RECORD,,
36
+ appkit_assistant/state/thread_state.py,sha256=xLydHA5YDFj3J1fGBQoAZsWg3zlcKOh2-9SzvlGvyU0,34742
37
+ appkit_assistant-0.17.0.dist-info/METADATA,sha256=jzEwclPQN3VXn5APgK9EQMLiGKnug-wI-JRxPu44Kao,9498
38
+ appkit_assistant-0.17.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
39
+ appkit_assistant-0.17.0.dist-info/RECORD,,