flowent 0.1.3 → 0.1.5

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.
Files changed (71) hide show
  1. package/backend/pyproject.toml +1 -1
  2. package/backend/src/flowent/__pycache__/__init__.cpython-313.pyc +0 -0
  3. package/backend/src/flowent/__pycache__/_version.cpython-313.pyc +0 -0
  4. package/backend/src/flowent/__pycache__/agent.cpython-313.pyc +0 -0
  5. package/backend/src/flowent/__pycache__/approval.cpython-313.pyc +0 -0
  6. package/backend/src/flowent/__pycache__/channels.cpython-313.pyc +0 -0
  7. package/backend/src/flowent/__pycache__/cli.cpython-313.pyc +0 -0
  8. package/backend/src/flowent/__pycache__/compact.cpython-313.pyc +0 -0
  9. package/backend/src/flowent/__pycache__/context.cpython-313.pyc +0 -0
  10. package/backend/src/flowent/__pycache__/llm.cpython-313.pyc +0 -0
  11. package/backend/src/flowent/__pycache__/logging.cpython-313.pyc +0 -0
  12. package/backend/src/flowent/__pycache__/main.cpython-313.pyc +0 -0
  13. package/backend/src/flowent/__pycache__/mcp.cpython-313.pyc +0 -0
  14. package/backend/src/flowent/__pycache__/mcp_import.cpython-313.pyc +0 -0
  15. package/backend/src/flowent/__pycache__/patch.cpython-313.pyc +0 -0
  16. package/backend/src/flowent/__pycache__/paths.cpython-313.pyc +0 -0
  17. package/backend/src/flowent/__pycache__/permissions.cpython-313.pyc +0 -0
  18. package/backend/src/flowent/__pycache__/sandbox.cpython-313.pyc +0 -0
  19. package/backend/src/flowent/__pycache__/skills.cpython-313.pyc +0 -0
  20. package/backend/src/flowent/__pycache__/storage.cpython-313.pyc +0 -0
  21. package/backend/src/flowent/__pycache__/tools.cpython-313.pyc +0 -0
  22. package/backend/src/flowent/agent.py +23 -1
  23. package/backend/src/flowent/approval.py +148 -0
  24. package/backend/src/flowent/cli.py +16 -2
  25. package/backend/src/flowent/compact.py +183 -0
  26. package/backend/src/flowent/context.py +19 -1
  27. package/backend/src/flowent/llm.py +51 -11
  28. package/backend/src/flowent/logging.py +60 -0
  29. package/backend/src/flowent/main.py +696 -192
  30. package/backend/src/flowent/mcp.py +3 -1
  31. package/backend/src/flowent/patch.py +55 -31
  32. package/backend/src/flowent/paths.py +12 -0
  33. package/backend/src/flowent/permissions.py +185 -42
  34. package/backend/src/flowent/sandbox.py +146 -13
  35. package/backend/src/flowent/static/assets/index-Cl20cARb.css +2 -0
  36. package/backend/src/flowent/static/assets/index-dsDDsEym.js +81 -0
  37. package/backend/src/flowent/static/index.html +2 -2
  38. package/backend/src/flowent/storage.py +257 -9
  39. package/backend/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  40. package/backend/tests/__pycache__/test_agent_tools.cpython-313-pytest-9.0.3.pyc +0 -0
  41. package/backend/tests/__pycache__/test_approval.cpython-313-pytest-9.0.3.pyc +0 -0
  42. package/backend/tests/__pycache__/test_channels.cpython-313-pytest-9.0.3.pyc +0 -0
  43. package/backend/tests/__pycache__/test_health.cpython-313-pytest-9.0.3.pyc +0 -0
  44. package/backend/tests/__pycache__/test_llm_providers.cpython-313-pytest-9.0.3.pyc +0 -0
  45. package/backend/tests/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
  46. package/backend/tests/__pycache__/test_mcp.cpython-313-pytest-9.0.3.pyc +0 -0
  47. package/backend/tests/__pycache__/test_patch.cpython-313-pytest-9.0.3.pyc +0 -0
  48. package/backend/tests/__pycache__/test_permissions.cpython-313-pytest-9.0.3.pyc +0 -0
  49. package/backend/tests/__pycache__/test_persistence.cpython-313-pytest-9.0.3.pyc +0 -0
  50. package/backend/tests/__pycache__/test_skills.cpython-313-pytest-9.0.3.pyc +0 -0
  51. package/backend/tests/__pycache__/test_startup_requirements.cpython-313-pytest-9.0.3.pyc +0 -0
  52. package/backend/tests/__pycache__/test_workspace_chat.cpython-313-pytest-9.0.3.pyc +0 -0
  53. package/backend/tests/test_agent_tools.py +312 -1
  54. package/backend/tests/test_approval.py +283 -0
  55. package/backend/tests/test_llm_providers.py +216 -0
  56. package/backend/tests/test_logging.py +30 -0
  57. package/backend/tests/test_mcp.py +76 -10
  58. package/backend/tests/test_patch.py +112 -0
  59. package/backend/tests/test_permissions.py +198 -53
  60. package/backend/tests/test_persistence.py +78 -0
  61. package/backend/tests/test_startup_requirements.py +96 -0
  62. package/backend/tests/test_workspace_chat.py +1265 -144
  63. package/backend/uv.lock +1 -1
  64. package/dist/frontend/assets/index-Cl20cARb.css +2 -0
  65. package/dist/frontend/assets/index-dsDDsEym.js +81 -0
  66. package/dist/frontend/index.html +2 -2
  67. package/package.json +2 -2
  68. package/backend/src/flowent/static/assets/index-DjF2KBwE.js +0 -81
  69. package/backend/src/flowent/static/assets/index-P-bBpJG8.css +0 -2
  70. package/dist/frontend/assets/index-DjF2KBwE.js +0 -81
  71. package/dist/frontend/assets/index-P-bBpJG8.css +0 -2
@@ -6,8 +6,8 @@
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>Flowent</title>
8
8
  <meta name="description" content="Flowent application" />
9
- <script type="module" crossorigin src="/assets/index-DjF2KBwE.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-P-bBpJG8.css">
9
+ <script type="module" crossorigin src="/assets/index-dsDDsEym.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-Cl20cARb.css">
11
11
  </head>
12
12
  <body>
13
13
  <div id="root"></div>
@@ -1,10 +1,11 @@
1
1
  import json
2
2
  import sqlite3
3
3
  from pathlib import Path
4
+ from typing import Annotated, Literal
4
5
 
5
6
  from pydantic import BaseModel, ConfigDict, Field
6
7
 
7
- from flowent.llm import ProviderFormat, ReasoningEffort
8
+ from flowent.llm import ChatMessage, ProviderFormat, ReasoningEffort
8
9
  from flowent.paths import data_directory
9
10
 
10
11
 
@@ -89,6 +90,7 @@ class StoredProvider(BaseModel):
89
90
  class StoredSettings(BaseModel):
90
91
  model_config = ConfigDict(extra="forbid")
91
92
 
93
+ agent_prompt: str = Field(default="", exclude_if=lambda value: value == "")
92
94
  reasoning_effort: ReasoningEffort = ReasoningEffort.DEFAULT
93
95
  selected_model: str
94
96
  selected_provider_id: str
@@ -106,11 +108,64 @@ class StoredToolItem(BaseModel):
106
108
  data: dict[str, object] | None = None
107
109
 
108
110
 
111
+ class StoredThinkingOutputItem(BaseModel):
112
+ model_config = ConfigDict(extra="forbid")
113
+
114
+ content: str
115
+ id: str
116
+ type: Literal["thinking"]
117
+
118
+
119
+ class StoredTextOutputItem(BaseModel):
120
+ model_config = ConfigDict(extra="forbid")
121
+
122
+ content: str
123
+ id: str
124
+ type: Literal["text"]
125
+
126
+
127
+ class StoredErrorOutputItem(BaseModel):
128
+ model_config = ConfigDict(extra="forbid")
129
+
130
+ detail: str = Field(default="", exclude_if=lambda value: value == "")
131
+ id: str
132
+ message: str
133
+ title: str
134
+ type: Literal["error"]
135
+
136
+
137
+ class StoredToolOutputItem(BaseModel):
138
+ model_config = ConfigDict(extra="forbid")
139
+
140
+ id: str
141
+ tool: StoredToolItem
142
+ type: Literal["tool"]
143
+
144
+
145
+ StoredOutputItem = Annotated[
146
+ StoredThinkingOutputItem
147
+ | StoredTextOutputItem
148
+ | StoredErrorOutputItem
149
+ | StoredToolOutputItem,
150
+ Field(discriminator="type"),
151
+ ]
152
+
153
+
154
+ class StoredAssistantOutputGroup(BaseModel):
155
+ model_config = ConfigDict(extra="forbid")
156
+
157
+ id: str
158
+ items: list[StoredOutputItem]
159
+
160
+
109
161
  class StoredMessage(BaseModel):
110
162
  model_config = ConfigDict(extra="forbid")
111
163
 
112
164
  author: str
113
165
  content: str
166
+ groups: list[StoredAssistantOutputGroup] = Field(
167
+ default_factory=list, exclude_if=lambda value: value == []
168
+ )
114
169
  id: str
115
170
  status: str = Field(
116
171
  default="completed", exclude_if=lambda value: value == "completed"
@@ -119,6 +174,20 @@ class StoredMessage(BaseModel):
119
174
  tools: list[StoredToolItem] = Field(default_factory=list)
120
175
 
121
176
 
177
+ class StoredCompactionCheckpoint(BaseModel):
178
+ model_config = ConfigDict(extra="forbid")
179
+
180
+ created_at: int = 0
181
+ id: str
182
+ method: str
183
+ replacement_history: list[ChatMessage]
184
+ source_message_id: str | None = None
185
+ summary: str
186
+ token_after: int = 0
187
+ token_before: int = 0
188
+ trigger: str
189
+
190
+
122
191
  class StoredState(BaseModel):
123
192
  model_config = ConfigDict(extra="forbid")
124
193
 
@@ -172,7 +241,7 @@ class StateStore:
172
241
  ]
173
242
  settings_row = connection.execute(
174
243
  """
175
- SELECT selected_provider_id, selected_model, reasoning_effort
244
+ SELECT selected_provider_id, selected_model, reasoning_effort, agent_prompt
176
245
  FROM settings
177
246
  WHERE id = 1
178
247
  """
@@ -181,6 +250,10 @@ class StateStore:
181
250
  StoredMessage(
182
251
  author=row["author"],
183
252
  content=row["content"],
253
+ groups=[
254
+ StoredAssistantOutputGroup.model_validate(group)
255
+ for group in json.loads(row["groups"] or "[]")
256
+ ],
184
257
  id=row["id"],
185
258
  status=row["status"],
186
259
  thinking=row["thinking"],
@@ -191,7 +264,7 @@ class StateStore:
191
264
  )
192
265
  for row in connection.execute(
193
266
  """
194
- SELECT id, author, content, tools, thinking, status
267
+ SELECT id, author, content, tools, thinking, groups, status
195
268
  FROM messages
196
269
  ORDER BY position, id
197
270
  """
@@ -203,6 +276,7 @@ class StateStore:
203
276
  messages=messages,
204
277
  providers=providers,
205
278
  settings=StoredSettings(
279
+ agent_prompt=settings_row["agent_prompt"] if settings_row else "",
206
280
  reasoning_effort=settings_row["reasoning_effort"]
207
281
  if settings_row
208
282
  else ReasoningEffort.DEFAULT,
@@ -503,19 +577,22 @@ class StateStore:
503
577
  id,
504
578
  selected_provider_id,
505
579
  selected_model,
506
- reasoning_effort
580
+ reasoning_effort,
581
+ agent_prompt
507
582
  )
508
- VALUES (1, ?, ?, ?)
583
+ VALUES (1, ?, ?, ?, ?)
509
584
  ON CONFLICT(id) DO UPDATE SET
510
585
  selected_provider_id = excluded.selected_provider_id,
511
586
  selected_model = excluded.selected_model,
512
587
  reasoning_effort = excluded.reasoning_effort,
588
+ agent_prompt = excluded.agent_prompt,
513
589
  updated_at = unixepoch()
514
590
  """,
515
591
  (
516
592
  settings.selected_provider_id,
517
593
  settings.selected_model,
518
594
  settings.reasoning_effort.value,
595
+ settings.agent_prompt,
519
596
  ),
520
597
  )
521
598
  return settings
@@ -525,8 +602,17 @@ class StateStore:
525
602
  connection.execute("DELETE FROM messages")
526
603
  connection.executemany(
527
604
  """
528
- INSERT INTO messages (id, author, content, tools, thinking, status, position)
529
- VALUES (?, ?, ?, ?, ?, ?, ?)
605
+ INSERT INTO messages (
606
+ id,
607
+ author,
608
+ content,
609
+ tools,
610
+ thinking,
611
+ groups,
612
+ status,
613
+ position
614
+ )
615
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
530
616
  """,
531
617
  [
532
618
  (
@@ -540,6 +626,13 @@ class StateStore:
540
626
  ]
541
627
  ),
542
628
  message.thinking,
629
+ json.dumps(
630
+ [
631
+ group.model_dump(exclude_none=True)
632
+ for group in message.groups
633
+ ],
634
+ ensure_ascii=False,
635
+ ),
543
636
  message.status,
544
637
  position,
545
638
  )
@@ -564,13 +657,23 @@ class StateStore:
564
657
  position = position_row["position"]
565
658
  connection.execute(
566
659
  """
567
- INSERT INTO messages (id, author, content, tools, thinking, status, position)
568
- VALUES (?, ?, ?, ?, ?, ?, ?)
660
+ INSERT INTO messages (
661
+ id,
662
+ author,
663
+ content,
664
+ tools,
665
+ thinking,
666
+ groups,
667
+ status,
668
+ position
669
+ )
670
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
569
671
  ON CONFLICT(id) DO UPDATE SET
570
672
  author = excluded.author,
571
673
  content = excluded.content,
572
674
  tools = excluded.tools,
573
675
  thinking = excluded.thinking,
676
+ groups = excluded.groups,
574
677
  status = excluded.status,
575
678
  position = excluded.position
576
679
  """,
@@ -582,6 +685,13 @@ class StateStore:
582
685
  [tool.model_dump(exclude_none=True) for tool in message.tools]
583
686
  ),
584
687
  message.thinking,
688
+ json.dumps(
689
+ [
690
+ group.model_dump(exclude_none=True)
691
+ for group in message.groups
692
+ ],
693
+ ensure_ascii=False,
694
+ ),
585
695
  message.status,
586
696
  position,
587
697
  ),
@@ -607,12 +717,120 @@ class StateStore:
607
717
  VALUES (1, ?)
608
718
  ON CONFLICT(id) DO UPDATE SET
609
719
  compacted_summary = excluded.compacted_summary,
720
+ active_compaction_id = NULL,
610
721
  updated_at = unixepoch()
611
722
  """,
612
723
  (summary,),
613
724
  )
614
725
  return summary
615
726
 
727
+ def read_active_compaction_checkpoint(
728
+ self,
729
+ ) -> StoredCompactionCheckpoint | None:
730
+ with self.connect() as connection:
731
+ row = connection.execute(
732
+ """
733
+ SELECT
734
+ checkpoint.id,
735
+ checkpoint.trigger,
736
+ checkpoint.method,
737
+ checkpoint.summary,
738
+ checkpoint.replacement_history,
739
+ checkpoint.source_message_id,
740
+ checkpoint.token_before,
741
+ checkpoint.token_after,
742
+ checkpoint.created_at
743
+ FROM workspace_context context
744
+ JOIN compaction_checkpoints checkpoint
745
+ ON checkpoint.id = context.active_compaction_id
746
+ WHERE context.id = 1
747
+ """
748
+ ).fetchone()
749
+ if row is None:
750
+ return None
751
+ return StoredCompactionCheckpoint(
752
+ created_at=row["created_at"],
753
+ id=row["id"],
754
+ method=row["method"],
755
+ replacement_history=[
756
+ ChatMessage.model_validate(message)
757
+ for message in json.loads(row["replacement_history"] or "[]")
758
+ ],
759
+ source_message_id=row["source_message_id"],
760
+ summary=row["summary"],
761
+ token_after=row["token_after"],
762
+ token_before=row["token_before"],
763
+ trigger=row["trigger"],
764
+ )
765
+
766
+ def save_compaction_checkpoint(
767
+ self, checkpoint: StoredCompactionCheckpoint
768
+ ) -> StoredCompactionCheckpoint:
769
+ with self.connect() as connection:
770
+ connection.execute(
771
+ """
772
+ INSERT INTO compaction_checkpoints (
773
+ id,
774
+ trigger,
775
+ method,
776
+ summary,
777
+ replacement_history,
778
+ source_message_id,
779
+ token_before,
780
+ token_after
781
+ )
782
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
783
+ ON CONFLICT(id) DO UPDATE SET
784
+ trigger = excluded.trigger,
785
+ method = excluded.method,
786
+ summary = excluded.summary,
787
+ replacement_history = excluded.replacement_history,
788
+ source_message_id = excluded.source_message_id,
789
+ token_before = excluded.token_before,
790
+ token_after = excluded.token_after
791
+ """,
792
+ (
793
+ checkpoint.id,
794
+ checkpoint.trigger,
795
+ checkpoint.method,
796
+ checkpoint.summary,
797
+ json.dumps(
798
+ [
799
+ message.model_dump()
800
+ for message in checkpoint.replacement_history
801
+ ],
802
+ ensure_ascii=False,
803
+ ),
804
+ checkpoint.source_message_id,
805
+ checkpoint.token_before,
806
+ checkpoint.token_after,
807
+ ),
808
+ )
809
+ connection.execute(
810
+ """
811
+ INSERT INTO workspace_context (
812
+ id,
813
+ compacted_summary,
814
+ active_compaction_id
815
+ )
816
+ VALUES (1, ?, ?)
817
+ ON CONFLICT(id) DO UPDATE SET
818
+ compacted_summary = excluded.compacted_summary,
819
+ active_compaction_id = excluded.active_compaction_id,
820
+ updated_at = unixepoch()
821
+ """,
822
+ (checkpoint.summary, checkpoint.id),
823
+ )
824
+ row = connection.execute(
825
+ """
826
+ SELECT created_at
827
+ FROM compaction_checkpoints
828
+ WHERE id = ?
829
+ """,
830
+ (checkpoint.id,),
831
+ ).fetchone()
832
+ return checkpoint.model_copy(update={"created_at": row["created_at"]})
833
+
616
834
  def _provider_models(
617
835
  self, connection: sqlite3.Connection, provider_id: str
618
836
  ) -> list[str]:
@@ -793,6 +1011,7 @@ class StateStore:
793
1011
  selected_provider_id TEXT NOT NULL DEFAULT '',
794
1012
  selected_model TEXT NOT NULL DEFAULT '',
795
1013
  reasoning_effort TEXT NOT NULL DEFAULT 'default',
1014
+ agent_prompt TEXT NOT NULL DEFAULT '',
796
1015
  updated_at INTEGER NOT NULL DEFAULT (unixepoch())
797
1016
  );
798
1017
 
@@ -807,9 +1026,22 @@ class StateStore:
807
1026
  CREATE TABLE IF NOT EXISTS workspace_context (
808
1027
  id INTEGER PRIMARY KEY CHECK (id = 1),
809
1028
  compacted_summary TEXT NOT NULL DEFAULT '',
1029
+ active_compaction_id TEXT,
810
1030
  updated_at INTEGER NOT NULL DEFAULT (unixepoch())
811
1031
  );
812
1032
 
1033
+ CREATE TABLE IF NOT EXISTS compaction_checkpoints (
1034
+ id TEXT PRIMARY KEY,
1035
+ trigger TEXT NOT NULL,
1036
+ method TEXT NOT NULL,
1037
+ summary TEXT NOT NULL,
1038
+ replacement_history TEXT NOT NULL DEFAULT '[]',
1039
+ source_message_id TEXT,
1040
+ token_before INTEGER NOT NULL DEFAULT 0,
1041
+ token_after INTEGER NOT NULL DEFAULT 0,
1042
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
1043
+ );
1044
+
813
1045
  CREATE TABLE IF NOT EXISTS skill_settings (
814
1046
  id TEXT PRIMARY KEY,
815
1047
  enabled INTEGER NOT NULL DEFAULT 1,
@@ -853,6 +1085,10 @@ class StateStore:
853
1085
  connection.execute(
854
1086
  "ALTER TABLE messages ADD COLUMN status TEXT NOT NULL DEFAULT 'completed'"
855
1087
  )
1088
+ if "groups" not in columns:
1089
+ connection.execute(
1090
+ "ALTER TABLE messages ADD COLUMN groups TEXT NOT NULL DEFAULT '[]'"
1091
+ )
856
1092
  settings_columns = {
857
1093
  row["name"] for row in connection.execute("PRAGMA table_info(settings)")
858
1094
  }
@@ -861,3 +1097,15 @@ class StateStore:
861
1097
  "ALTER TABLE settings "
862
1098
  "ADD COLUMN reasoning_effort TEXT NOT NULL DEFAULT 'default'"
863
1099
  )
1100
+ if "agent_prompt" not in settings_columns:
1101
+ connection.execute(
1102
+ "ALTER TABLE settings ADD COLUMN agent_prompt TEXT NOT NULL DEFAULT ''"
1103
+ )
1104
+ workspace_context_columns = {
1105
+ row["name"]
1106
+ for row in connection.execute("PRAGMA table_info(workspace_context)")
1107
+ }
1108
+ if "active_compaction_id" not in workspace_context_columns:
1109
+ connection.execute(
1110
+ "ALTER TABLE workspace_context ADD COLUMN active_compaction_id TEXT"
1111
+ )