soliplex 0.42__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.
- soliplex/agents.py +137 -0
- soliplex/agui/__init__.py +380 -0
- soliplex/agui/features.py +47 -0
- soliplex/agui/parser.py +577 -0
- soliplex/agui/persistence.py +938 -0
- soliplex/agui/util.py +10 -0
- soliplex/auth.py +10 -0
- soliplex/authn.py +77 -0
- soliplex/authz/__init__.py +92 -0
- soliplex/authz/schema.py +460 -0
- soliplex/cli.py +992 -0
- soliplex/completions.py +93 -0
- soliplex/config.py +2960 -0
- soliplex/examples.py +273 -0
- soliplex/haiku_chat.py +257 -0
- soliplex/installation.py +478 -0
- soliplex/log_ingest.py +48 -0
- soliplex/loggers.py +82 -0
- soliplex/main.py +225 -0
- soliplex/mcp_auth.py +105 -0
- soliplex/mcp_client.py +94 -0
- soliplex/mcp_server.py +94 -0
- soliplex/models.py +725 -0
- soliplex/ollama.py +179 -0
- soliplex/quizzes.py +107 -0
- soliplex/secrets.py +170 -0
- soliplex/tools.py +192 -0
- soliplex/tui/cli.py +47 -0
- soliplex/tui/main.py +1273 -0
- soliplex/tui/rest_api.py +266 -0
- soliplex/tui/serve.py +10 -0
- soliplex/util.py +244 -0
- soliplex/views/__init__.py +114 -0
- soliplex/views/agui.py +754 -0
- soliplex/views/authn.py +171 -0
- soliplex/views/authz.py +163 -0
- soliplex/views/completions.py +97 -0
- soliplex/views/installation.py +138 -0
- soliplex/views/log_ingest.py +67 -0
- soliplex/views/quizzes.py +107 -0
- soliplex/views/rooms.py +308 -0
- soliplex/views/streaming.py +53 -0
- soliplex-0.42.dist-info/METADATA +357 -0
- soliplex-0.42.dist-info/RECORD +48 -0
- soliplex-0.42.dist-info/WHEEL +5 -0
- soliplex-0.42.dist-info/entry_points.txt +4 -0
- soliplex-0.42.dist-info/licenses/LICENSE +21 -0
- soliplex-0.42.dist-info/top_level.txt +1 -0
soliplex/agents.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import typing
|
|
3
|
+
from collections import abc
|
|
4
|
+
|
|
5
|
+
import pydantic_ai
|
|
6
|
+
from pydantic_ai import agent as ai_agent
|
|
7
|
+
from pydantic_ai import mcp as ai_mcp
|
|
8
|
+
from pydantic_ai import tools as ai_tools
|
|
9
|
+
from pydantic_ai.models import google as google_models
|
|
10
|
+
from pydantic_ai.models import openai as openai_models
|
|
11
|
+
from pydantic_ai.providers import google as google_providers
|
|
12
|
+
from pydantic_ai.providers import ollama as ollama_providers
|
|
13
|
+
from pydantic_ai.providers import openai as openai_providers
|
|
14
|
+
|
|
15
|
+
from soliplex import agui
|
|
16
|
+
from soliplex import config
|
|
17
|
+
from soliplex import mcp_client
|
|
18
|
+
from soliplex import models
|
|
19
|
+
|
|
20
|
+
ToolConfigMap = dict[str, typing.Any]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclasses.dataclass
|
|
24
|
+
class AgentDependencies:
|
|
25
|
+
"""Agent dependencies implementing StateHandler protocol.
|
|
26
|
+
|
|
27
|
+
The `state` field is required by pydantic-ai's StateHandler protocol.
|
|
28
|
+
AG-UI will inject the client's state into this field for each run.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
the_installation: typing.Any # installation.Installation
|
|
32
|
+
user: models.UserProfile = None # TBD make required
|
|
33
|
+
tool_configs: ToolConfigMap = None
|
|
34
|
+
state: agui.AGUI_State = dataclasses.field(default_factory=dict)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
SoliplexAgent = ai_agent.AbstractAgent[AgentDependencies, typing.Any]
|
|
38
|
+
AgentFactory = abc.Callable[
|
|
39
|
+
[
|
|
40
|
+
config.AgentConfig,
|
|
41
|
+
ToolConfigMap,
|
|
42
|
+
config.MCP_ClientToolsetConfigMap,
|
|
43
|
+
],
|
|
44
|
+
SoliplexAgent,
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
# Cache for agents to avoid recreating them
|
|
48
|
+
_agent_cache: dict[str, pydantic_ai.Agent] = {}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def make_ai_tool(tool_config: config.ToolConfig) -> ai_tools.Tool:
|
|
52
|
+
tool_func = tool_config.tool_with_config
|
|
53
|
+
|
|
54
|
+
return ai_tools.Tool(
|
|
55
|
+
tool_func,
|
|
56
|
+
name=tool_config.tool_id,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def make_mcp_client_toolset(
|
|
61
|
+
toolset_config: config.MCP_ClientToolsetConfig,
|
|
62
|
+
) -> ai_mcp.MCPServer:
|
|
63
|
+
toolset_klass = mcp_client.TOOLSET_CLASS_BY_KIND[toolset_config.kind]
|
|
64
|
+
return toolset_klass(**toolset_config.tool_kwargs)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _get_default_agent_from_configs(
|
|
68
|
+
agent_config: config.AgentConfig,
|
|
69
|
+
tool_configs: ToolConfigMap,
|
|
70
|
+
mcp_client_toolset_configs: config.MCP_ClientToolsetConfigMap,
|
|
71
|
+
) -> SoliplexAgent:
|
|
72
|
+
"""Build a Pydantic AI agent from a config"""
|
|
73
|
+
provider_kw = agent_config.llm_provider_kw
|
|
74
|
+
|
|
75
|
+
if agent_config.provider_type == config.LLMProviderType.GOOGLE:
|
|
76
|
+
provider = google_providers.GoogleProvider(**provider_kw)
|
|
77
|
+
model = google_models.GoogleModel(
|
|
78
|
+
model_name=agent_config.model_name,
|
|
79
|
+
provider=provider,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
elif agent_config.provider_type == config.LLMProviderType.OLLAMA:
|
|
83
|
+
provider_kw["api_key"] = "dummy"
|
|
84
|
+
provider = ollama_providers.OllamaProvider(**provider_kw)
|
|
85
|
+
model = openai_models.OpenAIChatModel(
|
|
86
|
+
model_name=agent_config.model_name,
|
|
87
|
+
provider=provider,
|
|
88
|
+
)
|
|
89
|
+
else:
|
|
90
|
+
provider = openai_providers.OpenAIProvider(**provider_kw)
|
|
91
|
+
model = openai_models.OpenAIChatModel(
|
|
92
|
+
model_name=agent_config.model_name,
|
|
93
|
+
provider=provider,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
tools = [
|
|
97
|
+
make_ai_tool(tool_config) for tool_config in tool_configs.values()
|
|
98
|
+
]
|
|
99
|
+
toolsets = [
|
|
100
|
+
make_mcp_client_toolset(mctc)
|
|
101
|
+
for mctc in mcp_client_toolset_configs.values()
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
return pydantic_ai.Agent(
|
|
105
|
+
model=model,
|
|
106
|
+
model_settings=agent_config.model_settings,
|
|
107
|
+
tools=tools,
|
|
108
|
+
toolsets=toolsets,
|
|
109
|
+
instructions=agent_config.get_system_prompt(),
|
|
110
|
+
deps_type=AgentDependencies,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_agent_from_configs(
|
|
115
|
+
agent_config: config.AgentConfig,
|
|
116
|
+
tool_configs: ToolConfigMap,
|
|
117
|
+
mcp_client_toolset_configs: config.MCP_ClientToolsetConfigMap,
|
|
118
|
+
) -> SoliplexAgent:
|
|
119
|
+
"""Get or create an agent from the specified agent and tool configs."""
|
|
120
|
+
|
|
121
|
+
if agent_config.id not in _agent_cache:
|
|
122
|
+
if agent_config.kind == "default":
|
|
123
|
+
agent = _get_default_agent_from_configs(
|
|
124
|
+
agent_config,
|
|
125
|
+
tool_configs,
|
|
126
|
+
mcp_client_toolset_configs,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
else:
|
|
130
|
+
agent = agent_config.factory(
|
|
131
|
+
tool_configs=tool_configs,
|
|
132
|
+
mcp_client_toolset_configs=mcp_client_toolset_configs,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
_agent_cache[agent_config.id] = agent
|
|
136
|
+
|
|
137
|
+
return _agent_cache[agent_config.id]
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import collections.abc
|
|
5
|
+
import datetime
|
|
6
|
+
import typing
|
|
7
|
+
|
|
8
|
+
import fastapi
|
|
9
|
+
from ag_ui import core as agui_core
|
|
10
|
+
from sqlalchemy.ext import asyncio as sqla_asyncio
|
|
11
|
+
|
|
12
|
+
AGUI_Events = list[agui_core.Event]
|
|
13
|
+
AGUI_EventStream = collections.abc.AsyncIterator[agui_core.Event]
|
|
14
|
+
AGUI_State = dict[str, typing.Any]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_COMPACTIBLE_TYPES = {
|
|
18
|
+
agui_core.EventType.TEXT_MESSAGE_CONTENT,
|
|
19
|
+
agui_core.EventType.THINKING_TEXT_MESSAGE_CONTENT,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def compact_event_stream(stream: AGUI_EventStream):
|
|
24
|
+
compacting: agui_core.Event = None
|
|
25
|
+
compacting_id: str = None
|
|
26
|
+
|
|
27
|
+
async for event in stream:
|
|
28
|
+
if compacting is not None:
|
|
29
|
+
event_id = getattr(event, "message_id", None)
|
|
30
|
+
if event.type == compacting.type and event_id == compacting_id:
|
|
31
|
+
compacting.delta += event.delta
|
|
32
|
+
else:
|
|
33
|
+
to_yield, compacting = compacting, None
|
|
34
|
+
yield to_yield
|
|
35
|
+
yield event
|
|
36
|
+
|
|
37
|
+
else:
|
|
38
|
+
if event.type in _COMPACTIBLE_TYPES:
|
|
39
|
+
compacting = event.model_copy()
|
|
40
|
+
compacting_id = getattr(event, "message_id", None)
|
|
41
|
+
else:
|
|
42
|
+
yield event
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AGUI_Exception(ValueError):
|
|
46
|
+
status_code = 400
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class UnknownThread(AGUI_Exception):
|
|
50
|
+
status_code = 404
|
|
51
|
+
|
|
52
|
+
def __init__(self, user_name: str, thread_id: str):
|
|
53
|
+
self.user_name = user_name
|
|
54
|
+
self.thread_id = thread_id
|
|
55
|
+
message = f"Unknown thread: UUID {thread_id} for user {user_name}"
|
|
56
|
+
super().__init__(message)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class UnknownRun(AGUI_Exception):
|
|
60
|
+
status_code = 404
|
|
61
|
+
|
|
62
|
+
def __init__(self, run_id: str):
|
|
63
|
+
self.run_id = run_id
|
|
64
|
+
super().__init__(
|
|
65
|
+
f"Unknown run: UUID {run_id} does not exist in thread"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ThreadRoomMismatch(AGUI_Exception):
|
|
70
|
+
def __init__(self, room_id: str, thread_room_id: str):
|
|
71
|
+
self.room_id = room_id
|
|
72
|
+
self.thread_room_id = thread_room_id
|
|
73
|
+
super().__init__(
|
|
74
|
+
f"Thread room ID '{thread_room_id}' "
|
|
75
|
+
f"does not match room ID '{room_id}'"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class MissingParentRun(AGUI_Exception):
|
|
80
|
+
def __init__(self, parent_run_id: str):
|
|
81
|
+
self.parent_run_id = parent_run_id
|
|
82
|
+
super().__init__(
|
|
83
|
+
f"Unknown parent run: UUID {parent_run_id} "
|
|
84
|
+
f"does not exist in thread"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class RunAlreadyStarted(AGUI_Exception):
|
|
89
|
+
def __init__(self, user_name: str, thread_id: str, run_id: str):
|
|
90
|
+
self.user_name = user_name
|
|
91
|
+
self.thread_id = thread_id
|
|
92
|
+
self.run_id = run_id
|
|
93
|
+
super().__init__(f"Run already started: UUID {run_id}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
#
|
|
97
|
+
# ABCs defined here are notional contracts.
|
|
98
|
+
#
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
RunUsageStats = collections.namedtuple(
|
|
102
|
+
"RunUsageStats",
|
|
103
|
+
[
|
|
104
|
+
"input_tokens",
|
|
105
|
+
"output_tokens",
|
|
106
|
+
"requests",
|
|
107
|
+
"tool_calls",
|
|
108
|
+
],
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class RunUsage(abc.ABC):
|
|
113
|
+
"""LLM usage for a run"""
|
|
114
|
+
|
|
115
|
+
input_tokens: int
|
|
116
|
+
"""LLM input tokens consumed"""
|
|
117
|
+
|
|
118
|
+
output_tokens: int
|
|
119
|
+
"""LLM output tokens consumed"""
|
|
120
|
+
|
|
121
|
+
requests: int
|
|
122
|
+
"""LLM requests made"""
|
|
123
|
+
|
|
124
|
+
tool_calls: int
|
|
125
|
+
"""LLM tool_calls made"""
|
|
126
|
+
|
|
127
|
+
@abc.abstractmethod
|
|
128
|
+
def as_tuple(self) -> RunUsageStats:
|
|
129
|
+
"""Return values as a tuple."""
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class RunMetadata(abc.ABC):
|
|
133
|
+
"""User-defined metadata for a run"""
|
|
134
|
+
|
|
135
|
+
label: str
|
|
136
|
+
"""Label for a run (similar to a git tag)"""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class Run(abc.ABC):
|
|
140
|
+
"""Input data and events for an AGUI run
|
|
141
|
+
|
|
142
|
+
Runs are not accessed directly: use 'ThreadStorage'.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
thread_id: str
|
|
146
|
+
"""ID of the thread in which the run was created"""
|
|
147
|
+
|
|
148
|
+
run_id: str
|
|
149
|
+
"""Unique ID for a run"""
|
|
150
|
+
|
|
151
|
+
parent_run_id: str | None
|
|
152
|
+
"""ID of the parent run"""
|
|
153
|
+
|
|
154
|
+
run_usage: RunUsage | None
|
|
155
|
+
"""Optional LLM usage data for a run"""
|
|
156
|
+
|
|
157
|
+
run_metadata: RunMetadata | None
|
|
158
|
+
"""Optional user-defined metadata for a run"""
|
|
159
|
+
|
|
160
|
+
run_input: agui_core.RunAgentInput
|
|
161
|
+
"""Input from the client-request which initiates the AG-UI run"""
|
|
162
|
+
|
|
163
|
+
created: datetime.datetime
|
|
164
|
+
"""Timestamp"""
|
|
165
|
+
|
|
166
|
+
@abc.abstractmethod
|
|
167
|
+
async def list_events(self) -> AGUI_Events:
|
|
168
|
+
"""Return AGUI events for the run"""
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class ThreadMetadata(abc.ABC):
|
|
172
|
+
"""Optional user-defined thread metadata"""
|
|
173
|
+
|
|
174
|
+
name: str
|
|
175
|
+
"""Name for the thread"""
|
|
176
|
+
|
|
177
|
+
description: str
|
|
178
|
+
"""Description for the thread"""
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class Thread(abc.ABC):
|
|
182
|
+
"""Hold a set of AGUI runs sharing the same 'thread_id'
|
|
183
|
+
|
|
184
|
+
Runs are not accessed directly: use 'ThreadStorage'.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
thread_id: str
|
|
188
|
+
"""Unique ID for the thread"""
|
|
189
|
+
|
|
190
|
+
room_id: str
|
|
191
|
+
"""ID for room in which the thread was created"""
|
|
192
|
+
|
|
193
|
+
thread_metadata: ThreadMetadata | None
|
|
194
|
+
"""Optional thread metadata"""
|
|
195
|
+
|
|
196
|
+
created: datetime.datetime
|
|
197
|
+
"""Timestamp"""
|
|
198
|
+
|
|
199
|
+
@abc.abstractmethod
|
|
200
|
+
async def list_runs(self) -> list[Run]:
|
|
201
|
+
"""Return runs for this thread"""
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class ThreadStorage(abc.ABC):
|
|
205
|
+
@abc.abstractmethod
|
|
206
|
+
async def list_user_threads(
|
|
207
|
+
self,
|
|
208
|
+
*,
|
|
209
|
+
user_name: str,
|
|
210
|
+
room_id: str = None,
|
|
211
|
+
) -> list[Thread]:
|
|
212
|
+
"""Return a list of the user's threads.
|
|
213
|
+
|
|
214
|
+
If 'room_id' is passed, filter the threads to those created
|
|
215
|
+
in that room.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
@abc.abstractmethod
|
|
219
|
+
async def get_thread(
|
|
220
|
+
self,
|
|
221
|
+
*,
|
|
222
|
+
user_name: str,
|
|
223
|
+
room_id: str,
|
|
224
|
+
thread_id: str,
|
|
225
|
+
) -> Thread:
|
|
226
|
+
"""Return the actual thread instance
|
|
227
|
+
|
|
228
|
+
N.B.: caller must treat the instance as read-only!
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
@abc.abstractmethod
|
|
232
|
+
async def new_thread(
|
|
233
|
+
self,
|
|
234
|
+
*,
|
|
235
|
+
user_name: str,
|
|
236
|
+
room_id: str,
|
|
237
|
+
thread_metadata: ThreadMetadata | dict = None,
|
|
238
|
+
initial_run: bool = True,
|
|
239
|
+
) -> Thread:
|
|
240
|
+
"""Create a new thread
|
|
241
|
+
|
|
242
|
+
If 'thread_metadata' is a dict, convert it to a 'ThreadMetadata'
|
|
243
|
+
instance.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
@abc.abstractmethod
|
|
247
|
+
async def update_thread_metadata(
|
|
248
|
+
self,
|
|
249
|
+
*,
|
|
250
|
+
user_name: str,
|
|
251
|
+
room_id: str,
|
|
252
|
+
thread_id: str,
|
|
253
|
+
thread_metadata: ThreadMetadata | dict = None,
|
|
254
|
+
) -> Thread:
|
|
255
|
+
"""Update thread instance with the given metadata, or None
|
|
256
|
+
|
|
257
|
+
If 'thread_metadata' is a dict, convert it to a 'ThreadMetadata'
|
|
258
|
+
instance.
|
|
259
|
+
|
|
260
|
+
If 'thread_metadata' is None, or an empty dict, remove any existing
|
|
261
|
+
metadata on the thread.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
@abc.abstractmethod
|
|
265
|
+
async def delete_thread(
|
|
266
|
+
self,
|
|
267
|
+
*,
|
|
268
|
+
user_name: str,
|
|
269
|
+
room_id: str,
|
|
270
|
+
thread_id: str,
|
|
271
|
+
) -> None:
|
|
272
|
+
"""Remove a thread"""
|
|
273
|
+
|
|
274
|
+
@abc.abstractmethod
|
|
275
|
+
async def new_run(
|
|
276
|
+
self,
|
|
277
|
+
*,
|
|
278
|
+
user_name: str,
|
|
279
|
+
room_id: str,
|
|
280
|
+
thread_id: str,
|
|
281
|
+
run_metadata: RunMetadata = None,
|
|
282
|
+
parent_run_id: str = None,
|
|
283
|
+
) -> Run:
|
|
284
|
+
"""Create a new run for the thread
|
|
285
|
+
|
|
286
|
+
If 'run_metadata' is a dict, convert it to a 'RunMetadata' instance.
|
|
287
|
+
|
|
288
|
+
If 'parent_run_id' is passed, ensure it is valid.
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
@abc.abstractmethod
|
|
292
|
+
async def get_run(
|
|
293
|
+
self,
|
|
294
|
+
user_name: str,
|
|
295
|
+
room_id: str,
|
|
296
|
+
thread_id: str,
|
|
297
|
+
run_id: str,
|
|
298
|
+
) -> Run:
|
|
299
|
+
"""Return an existing run for a thread"""
|
|
300
|
+
|
|
301
|
+
@abc.abstractmethod
|
|
302
|
+
async def add_run_input(
|
|
303
|
+
self,
|
|
304
|
+
*,
|
|
305
|
+
user_name: str,
|
|
306
|
+
room_id: str,
|
|
307
|
+
thread_id: str,
|
|
308
|
+
run_id: str,
|
|
309
|
+
run_input: agui_core.RunAgentInput,
|
|
310
|
+
) -> Run:
|
|
311
|
+
"""Update a run with the given 'run_agent_input'"""
|
|
312
|
+
|
|
313
|
+
@abc.abstractmethod
|
|
314
|
+
async def update_run_metadata(
|
|
315
|
+
self,
|
|
316
|
+
*,
|
|
317
|
+
user_name: str,
|
|
318
|
+
room_id: str,
|
|
319
|
+
thread_id: str,
|
|
320
|
+
run_id: str,
|
|
321
|
+
run_metadata: RunMetadata | dict = None,
|
|
322
|
+
) -> Run:
|
|
323
|
+
"""Update a run with the given metadata
|
|
324
|
+
|
|
325
|
+
If 'run_metadata' is a dict, convert it to a 'RunMetadata' instance.
|
|
326
|
+
|
|
327
|
+
If 'run_metadata' is None, or an empty dict, remove any existing
|
|
328
|
+
metadata on the run.
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
@abc.abstractmethod
|
|
332
|
+
async def save_run_events(
|
|
333
|
+
self,
|
|
334
|
+
*,
|
|
335
|
+
user_name: str,
|
|
336
|
+
room_id: str,
|
|
337
|
+
thread_id: str,
|
|
338
|
+
run_id: str,
|
|
339
|
+
events: AGUI_Events,
|
|
340
|
+
) -> AGUI_Events:
|
|
341
|
+
"""Save the events for a gven run"""
|
|
342
|
+
|
|
343
|
+
@abc.abstractmethod
|
|
344
|
+
async def save_run_usage(
|
|
345
|
+
self,
|
|
346
|
+
*,
|
|
347
|
+
user_name: str,
|
|
348
|
+
room_id: str,
|
|
349
|
+
thread_id: str,
|
|
350
|
+
run_id: str,
|
|
351
|
+
input_tokens: int,
|
|
352
|
+
output_tokens: int,
|
|
353
|
+
requests: int,
|
|
354
|
+
tool_calls: int,
|
|
355
|
+
):
|
|
356
|
+
"""Save the run usage statistics"""
|
|
357
|
+
|
|
358
|
+
@abc.abstractmethod
|
|
359
|
+
async def save_run_feedback(
|
|
360
|
+
self,
|
|
361
|
+
*,
|
|
362
|
+
user_name: str,
|
|
363
|
+
room_id: str,
|
|
364
|
+
thread_id: str,
|
|
365
|
+
run_id: str,
|
|
366
|
+
feedback: str,
|
|
367
|
+
reason: str,
|
|
368
|
+
):
|
|
369
|
+
"""Save the run feedback"""
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
async def get_the_threads(request: fastapi.Request) -> ThreadStorage:
|
|
373
|
+
from . import persistence
|
|
374
|
+
|
|
375
|
+
engine = request.state.threads_engine
|
|
376
|
+
async with sqla_asyncio.AsyncSession(bind=engine) as session:
|
|
377
|
+
yield persistence.ThreadStorage(session)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
depend_the_threads = fastapi.Depends(get_the_threads)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Features defined by soliplex"""
|
|
2
|
+
|
|
3
|
+
import pydantic
|
|
4
|
+
from haiku.rag.agents.chat import state as hr_chat_state
|
|
5
|
+
from haiku.rag.agents.research import models as hr_graph_models
|
|
6
|
+
|
|
7
|
+
FILTER_DOCUMENTS_FEATURE = "filter_documents"
|
|
8
|
+
ASK_HISTORY_FEATURE = "ask_history"
|
|
9
|
+
HAIKU_CHAT_FEATURE = hr_chat_state.AGUI_STATE_KEY
|
|
10
|
+
|
|
11
|
+
KW_ONLY_NONE = pydantic.Field(kw_only=True, default=None)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FilterDocuments(pydantic.BaseModel):
|
|
15
|
+
"""Documents selected by the user to be used to answer a question
|
|
16
|
+
|
|
17
|
+
This model describes the 'filter_documents' key in the AG-UI state.
|
|
18
|
+
|
|
19
|
+
If 'document_ids' is empty or None, or if the 'filter_documents'
|
|
20
|
+
key is not present in the AG-UI state, no filter is applied: the
|
|
21
|
+
'ask_with_rich_citations' tool will return all documents matching
|
|
22
|
+
the query from the LLM.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
document_ids: list[str] | None = KW_ONLY_NONE
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class QuestionResponseCitations(pydantic.BaseModel):
|
|
29
|
+
"""Single question to the 'ask_with_rich_citations' tool
|
|
30
|
+
|
|
31
|
+
Includes response generated by the tool and accompanying citations.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
question: str
|
|
35
|
+
response: str
|
|
36
|
+
citations: list[hr_graph_models.Citation] = []
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AskedAndAnswered(pydantic.BaseModel):
|
|
40
|
+
"""History of questions asked and their replies + citations
|
|
41
|
+
|
|
42
|
+
This model describes the 'ask_history' key in the AG-UI state.
|
|
43
|
+
|
|
44
|
+
Each call to the 'ask_with_rich_citations' took append
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
questions: list[QuestionResponseCitations] = []
|