hyperforge 1.0.0.post19__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.
- hyperforge/__init__.py +16 -0
- hyperforge/agent.py +81 -0
- hyperforge/api/__init__.py +20 -0
- hyperforge/api/app.py +155 -0
- hyperforge/api/authentication.py +271 -0
- hyperforge/api/commands.py +33 -0
- hyperforge/api/internal/__init__.py +4 -0
- hyperforge/api/internal/inspect.py +30 -0
- hyperforge/api/internal/router.py +3 -0
- hyperforge/api/logging.py +18 -0
- hyperforge/api/models.py +129 -0
- hyperforge/api/session.py +197 -0
- hyperforge/api/settings.py +38 -0
- hyperforge/api/utils.py +354 -0
- hyperforge/api/v1/__init__.py +23 -0
- hyperforge/api/v1/agents.py +531 -0
- hyperforge/api/v1/interaction.py +430 -0
- hyperforge/api/v1/mcp_content.py +311 -0
- hyperforge/api/v1/mcp_interaction.py +322 -0
- hyperforge/api/v1/oauth.py +60 -0
- hyperforge/api/v1/prompt.py +129 -0
- hyperforge/api/v1/router.py +3 -0
- hyperforge/api/v1/schema.py +56 -0
- hyperforge/api/v1/session.py +182 -0
- hyperforge/api/v1/utils.py +12 -0
- hyperforge/api/v1/workflows.py +643 -0
- hyperforge/arag.py +28 -0
- hyperforge/broker/__init__.py +52 -0
- hyperforge/broker/local.py +116 -0
- hyperforge/broker/redis.py +161 -0
- hyperforge/configure.py +571 -0
- hyperforge/context/__init__.py +0 -0
- hyperforge/context/agent.py +377 -0
- hyperforge/context/config.py +103 -0
- hyperforge/database.py +3 -0
- hyperforge/db/__init__.py +6 -0
- hyperforge/db/agents.py +1521 -0
- hyperforge/db/encryption.py +91 -0
- hyperforge/db/exceptions.py +26 -0
- hyperforge/db/settings.py +16 -0
- hyperforge/db/workflow_cleanup.py +69 -0
- hyperforge/definition.py +13 -0
- hyperforge/driver.py +31 -0
- hyperforge/dummy.py +28 -0
- hyperforge/engine.py +189 -0
- hyperforge/exceptions.py +14 -0
- hyperforge/feature_flag.py +105 -0
- hyperforge/fixtures.py +602 -0
- hyperforge/interaction.py +116 -0
- hyperforge/llm.py +75 -0
- hyperforge/manager.py +432 -0
- hyperforge/memory/__init__.py +5 -0
- hyperforge/memory/memory.py +974 -0
- hyperforge/minimal_fixtures.py +75 -0
- hyperforge/models.py +336 -0
- hyperforge/nua.py +336 -0
- hyperforge/openapi.py +63 -0
- hyperforge/prompts.py +188 -0
- hyperforge/pubsub.py +90 -0
- hyperforge/py.typed +0 -0
- hyperforge/redis_utils.py +82 -0
- hyperforge/retrieval/__init__.py +0 -0
- hyperforge/retrieval/agent.py +169 -0
- hyperforge/retrieval/config.py +94 -0
- hyperforge/server/__init__.py +5 -0
- hyperforge/server/cache.py +131 -0
- hyperforge/server/run.py +109 -0
- hyperforge/server/sandbox.py +60 -0
- hyperforge/server/session.py +421 -0
- hyperforge/server/settings.py +47 -0
- hyperforge/server/utils.py +57 -0
- hyperforge/server/web.py +31 -0
- hyperforge/settings.py +18 -0
- hyperforge/standalone/__init__.py +5 -0
- hyperforge/standalone/agent.py +189 -0
- hyperforge/standalone/app.py +264 -0
- hyperforge/standalone/config.py +137 -0
- hyperforge/standalone/const.py +1 -0
- hyperforge/standalone/run.py +60 -0
- hyperforge/standalone/settings.py +133 -0
- hyperforge/standalone/ui_router.py +241 -0
- hyperforge/trace.py +42 -0
- hyperforge/utils/__init__.py +112 -0
- hyperforge/utils/http.py +48 -0
- hyperforge/workflows.py +44 -0
- hyperforge-1.0.0.post19.dist-info/METADATA +95 -0
- hyperforge-1.0.0.post19.dist-info/RECORD +90 -0
- hyperforge-1.0.0.post19.dist-info/WHEEL +5 -0
- hyperforge-1.0.0.post19.dist-info/entry_points.txt +8 -0
- hyperforge-1.0.0.post19.dist-info/top_level.txt +1 -0
hyperforge/nua.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import (
|
|
3
|
+
Any,
|
|
4
|
+
AsyncIterator,
|
|
5
|
+
Optional,
|
|
6
|
+
Type,
|
|
7
|
+
TypeVar,
|
|
8
|
+
Union,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
import backoff
|
|
12
|
+
from deprecated import deprecated
|
|
13
|
+
from httpx import AsyncClient
|
|
14
|
+
from nuclia.exceptions import NuaAPIException
|
|
15
|
+
from nuclia.lib.nua_responses import (
|
|
16
|
+
ChatModel,
|
|
17
|
+
ChatResponse,
|
|
18
|
+
QueryInfo,
|
|
19
|
+
RephraseModel,
|
|
20
|
+
RerankModel,
|
|
21
|
+
RerankResponse,
|
|
22
|
+
Sentence,
|
|
23
|
+
SummarizedModel,
|
|
24
|
+
SummarizeModel,
|
|
25
|
+
SummarizeResource,
|
|
26
|
+
Tokens,
|
|
27
|
+
)
|
|
28
|
+
from nuclia_models.common.consumption import Consumption, ConsumptionGenerative
|
|
29
|
+
from nuclia_models.predict.generative_responses import (
|
|
30
|
+
CitationsGenerativeResponse,
|
|
31
|
+
GenerativeChunk,
|
|
32
|
+
GenerativeFullResponse,
|
|
33
|
+
JSONGenerativeResponse,
|
|
34
|
+
MetaGenerativeResponse,
|
|
35
|
+
StatusGenerativeResponse,
|
|
36
|
+
TextGenerativeResponse,
|
|
37
|
+
ToolsGenerativeResponse,
|
|
38
|
+
)
|
|
39
|
+
from nuclia_models.predict.remi import RemiRequest, RemiResponse
|
|
40
|
+
from pydantic import BaseModel
|
|
41
|
+
|
|
42
|
+
MB = 1024 * 1024
|
|
43
|
+
CHUNK_SIZE = 10 * MB
|
|
44
|
+
SENTENCE_PREDICT = "/api/v1/internal/predict/sentence"
|
|
45
|
+
CHAT_PREDICT = "/api/v1/internal/predict/chat"
|
|
46
|
+
SUMMARIZE_PREDICT = "/api/v1/internal/predict/summarize"
|
|
47
|
+
REPHRASE_PREDICT = "/api/v1/internal/predict/rephrase"
|
|
48
|
+
TOKENS_PREDICT = "/api/v1/internal/predict/tokens"
|
|
49
|
+
QUERY_PREDICT = "/api/v1/internal/predict/query"
|
|
50
|
+
REMI_PREDICT = "/api/v1/internal/predict/remi"
|
|
51
|
+
AGENTS_PREDICT = "/api/v1/internal/predict/run-agents"
|
|
52
|
+
RERANK = "/api/v1/internal/predict/rerank"
|
|
53
|
+
|
|
54
|
+
ConvertType = TypeVar("ConvertType", bound=BaseModel)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Author(str, Enum):
|
|
58
|
+
NUCLIA = "NUCLIA"
|
|
59
|
+
USER = "USER"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ContextItem(BaseModel):
|
|
63
|
+
author: Author
|
|
64
|
+
text: str
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class RetriableRequestException(NuaAPIException):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class AsyncInternalNuaClient:
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
kbid: str | None,
|
|
75
|
+
account: str | None,
|
|
76
|
+
url: str,
|
|
77
|
+
):
|
|
78
|
+
self.headers = {"X-STF-KBID": kbid} if kbid else {}
|
|
79
|
+
if account:
|
|
80
|
+
self.headers["X-STF-ACCOUNT"] = account
|
|
81
|
+
|
|
82
|
+
self.stream_headers = self.headers.copy()
|
|
83
|
+
self.stream_headers["Accept"] = "application/x-ndjson"
|
|
84
|
+
|
|
85
|
+
self.url = url
|
|
86
|
+
self.client = AsyncClient(headers=self.headers, base_url=url)
|
|
87
|
+
self.stream_client = AsyncClient(headers=self.stream_headers, base_url=url)
|
|
88
|
+
|
|
89
|
+
@backoff.on_exception(
|
|
90
|
+
backoff.expo,
|
|
91
|
+
(RetriableRequestException,),
|
|
92
|
+
max_time=60,
|
|
93
|
+
jitter=backoff.full_jitter,
|
|
94
|
+
)
|
|
95
|
+
async def _request(
|
|
96
|
+
self,
|
|
97
|
+
method: str,
|
|
98
|
+
url: str,
|
|
99
|
+
output: Type[ConvertType],
|
|
100
|
+
payload: Optional[dict[Any, Any]] = None,
|
|
101
|
+
timeout: int = 60,
|
|
102
|
+
) -> ConvertType:
|
|
103
|
+
resp = await self.client.request(method, url, json=payload, timeout=timeout)
|
|
104
|
+
if resp.status_code in (429, 512):
|
|
105
|
+
raise RetriableRequestException(
|
|
106
|
+
code=resp.status_code, detail=resp.content.decode()
|
|
107
|
+
)
|
|
108
|
+
if resp.status_code > 299:
|
|
109
|
+
raise NuaAPIException(code=resp.status_code, detail=resp.content.decode())
|
|
110
|
+
try:
|
|
111
|
+
data = output.model_validate(resp.json())
|
|
112
|
+
except Exception:
|
|
113
|
+
data = output.model_validate(resp.content)
|
|
114
|
+
return data
|
|
115
|
+
|
|
116
|
+
@backoff.on_exception(
|
|
117
|
+
backoff.expo,
|
|
118
|
+
(RetriableRequestException,),
|
|
119
|
+
max_time=60,
|
|
120
|
+
jitter=backoff.full_jitter,
|
|
121
|
+
)
|
|
122
|
+
async def _stream(
|
|
123
|
+
self,
|
|
124
|
+
method: str,
|
|
125
|
+
url: str,
|
|
126
|
+
payload: Optional[dict[Any, Any]] = None,
|
|
127
|
+
timeout: int = 60,
|
|
128
|
+
extra_headers: Optional[dict[str, str]] = None,
|
|
129
|
+
) -> AsyncIterator[GenerativeChunk]:
|
|
130
|
+
async with self.stream_client.stream(
|
|
131
|
+
method,
|
|
132
|
+
url,
|
|
133
|
+
json=payload,
|
|
134
|
+
timeout=timeout,
|
|
135
|
+
headers=extra_headers,
|
|
136
|
+
) as response:
|
|
137
|
+
if response.status_code in (429, 512):
|
|
138
|
+
raise RetriableRequestException(
|
|
139
|
+
code=response.status_code,
|
|
140
|
+
detail=(await response.aread()).decode(errors="ignore"),
|
|
141
|
+
)
|
|
142
|
+
elif response.status_code > 299:
|
|
143
|
+
raise NuaAPIException(
|
|
144
|
+
code=response.status_code,
|
|
145
|
+
detail=(await response.aread()).decode(errors="ignore"),
|
|
146
|
+
)
|
|
147
|
+
async for json_body in response.aiter_lines():
|
|
148
|
+
yield GenerativeChunk.model_validate_json(json_body)
|
|
149
|
+
|
|
150
|
+
async def sentence_predict(
|
|
151
|
+
self, text: str, model: Optional[str] = None
|
|
152
|
+
) -> Sentence:
|
|
153
|
+
endpoint = f"{self.url}{SENTENCE_PREDICT}?text={text}"
|
|
154
|
+
if model:
|
|
155
|
+
endpoint += f"&model={model}"
|
|
156
|
+
return await self._request("GET", endpoint, output=Sentence)
|
|
157
|
+
|
|
158
|
+
async def tokens_predict(self, text: str, model: Optional[str] = None) -> Tokens:
|
|
159
|
+
endpoint = f"{self.url}{TOKENS_PREDICT}?text={text}"
|
|
160
|
+
if model:
|
|
161
|
+
endpoint += f"&model={model}"
|
|
162
|
+
return await self._request("GET", endpoint, output=Tokens)
|
|
163
|
+
|
|
164
|
+
async def query_predict(
|
|
165
|
+
self,
|
|
166
|
+
text: str,
|
|
167
|
+
semantic_model: Optional[str] = None,
|
|
168
|
+
token_model: Optional[str] = None,
|
|
169
|
+
generative_model: Optional[str] = None,
|
|
170
|
+
) -> QueryInfo:
|
|
171
|
+
endpoint = f"{self.url}{QUERY_PREDICT}?text={text}"
|
|
172
|
+
if semantic_model:
|
|
173
|
+
endpoint += f"&semantic_model={semantic_model}"
|
|
174
|
+
if token_model:
|
|
175
|
+
endpoint += f"&token_model={token_model}"
|
|
176
|
+
if generative_model:
|
|
177
|
+
endpoint += f"&generative_model={generative_model}"
|
|
178
|
+
return await self._request("GET", endpoint, output=QueryInfo)
|
|
179
|
+
|
|
180
|
+
@deprecated(version="2.1.0", reason="You should use generate function")
|
|
181
|
+
async def generate_predict(
|
|
182
|
+
self, body: ChatModel, model: Optional[str] = None, timeout: int = 300
|
|
183
|
+
) -> ChatResponse:
|
|
184
|
+
endpoint = f"{self.url}{CHAT_PREDICT}"
|
|
185
|
+
if model:
|
|
186
|
+
endpoint += f"?model={model}"
|
|
187
|
+
|
|
188
|
+
return await self._request(
|
|
189
|
+
"POST",
|
|
190
|
+
endpoint,
|
|
191
|
+
payload=body.model_dump(),
|
|
192
|
+
output=ChatResponse,
|
|
193
|
+
timeout=timeout,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
async def generate(
|
|
197
|
+
self,
|
|
198
|
+
body: ChatModel,
|
|
199
|
+
model: Optional[str] = None,
|
|
200
|
+
timeout: int = 300,
|
|
201
|
+
extra_headers: Optional[dict[str, str]] = None,
|
|
202
|
+
) -> GenerativeFullResponse:
|
|
203
|
+
endpoint = f"{self.url}{CHAT_PREDICT}"
|
|
204
|
+
if model:
|
|
205
|
+
endpoint += f"?model={model}"
|
|
206
|
+
result = GenerativeFullResponse(answer="")
|
|
207
|
+
async for chunk in self._stream(
|
|
208
|
+
"POST",
|
|
209
|
+
endpoint,
|
|
210
|
+
payload=body.model_dump(),
|
|
211
|
+
timeout=timeout,
|
|
212
|
+
extra_headers=extra_headers,
|
|
213
|
+
):
|
|
214
|
+
if isinstance(chunk.chunk, TextGenerativeResponse):
|
|
215
|
+
result.answer += chunk.chunk.text
|
|
216
|
+
elif isinstance(chunk.chunk, JSONGenerativeResponse):
|
|
217
|
+
result.object = chunk.chunk.object
|
|
218
|
+
elif isinstance(chunk.chunk, MetaGenerativeResponse):
|
|
219
|
+
result.timings = chunk.chunk.timings
|
|
220
|
+
elif isinstance(chunk.chunk, CitationsGenerativeResponse):
|
|
221
|
+
result.citations = chunk.chunk.citations
|
|
222
|
+
elif isinstance(chunk.chunk, StatusGenerativeResponse):
|
|
223
|
+
result.code = chunk.chunk.code
|
|
224
|
+
elif isinstance(chunk.chunk, ToolsGenerativeResponse):
|
|
225
|
+
result.tools = chunk.chunk.tools
|
|
226
|
+
elif isinstance(chunk.chunk, ConsumptionGenerative):
|
|
227
|
+
result.consumption = Consumption(
|
|
228
|
+
normalized_tokens=chunk.chunk.normalized_tokens,
|
|
229
|
+
customer_key_tokens=chunk.chunk.customer_key_tokens,
|
|
230
|
+
)
|
|
231
|
+
return result
|
|
232
|
+
|
|
233
|
+
async def generate_stream(
|
|
234
|
+
self,
|
|
235
|
+
body: ChatModel,
|
|
236
|
+
model: Optional[str] = None,
|
|
237
|
+
timeout: int = 300,
|
|
238
|
+
extra_headers: Optional[dict[str, str]] = None,
|
|
239
|
+
) -> AsyncIterator[GenerativeChunk]:
|
|
240
|
+
endpoint = f"{self.url}{CHAT_PREDICT}"
|
|
241
|
+
if model:
|
|
242
|
+
endpoint += f"?model={model}"
|
|
243
|
+
|
|
244
|
+
async for gr in self._stream(
|
|
245
|
+
"POST",
|
|
246
|
+
endpoint,
|
|
247
|
+
payload=body.model_dump(),
|
|
248
|
+
timeout=timeout,
|
|
249
|
+
):
|
|
250
|
+
yield gr
|
|
251
|
+
|
|
252
|
+
async def summarize(
|
|
253
|
+
self, documents: dict[str, str], model: Optional[str] = None, timeout: int = 300
|
|
254
|
+
) -> SummarizedModel:
|
|
255
|
+
endpoint = f"{self.url}{SUMMARIZE_PREDICT}"
|
|
256
|
+
if model:
|
|
257
|
+
endpoint += f"?model={model}"
|
|
258
|
+
|
|
259
|
+
body = SummarizeModel(
|
|
260
|
+
resources={
|
|
261
|
+
key: SummarizeResource(fields={"field": document})
|
|
262
|
+
for key, document in documents.items()
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
return await self._request(
|
|
266
|
+
"POST",
|
|
267
|
+
endpoint,
|
|
268
|
+
payload=body.model_dump(),
|
|
269
|
+
output=SummarizedModel,
|
|
270
|
+
timeout=timeout,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
async def rephrase(
|
|
274
|
+
self,
|
|
275
|
+
question: str,
|
|
276
|
+
user_context: Optional[list[str]] = None,
|
|
277
|
+
context: Optional[list[Union[dict, ContextItem]]] = None,
|
|
278
|
+
model: Optional[str] = None,
|
|
279
|
+
prompt: Optional[str] = None,
|
|
280
|
+
) -> RephraseModel:
|
|
281
|
+
endpoint = f"{self.url}{REPHRASE_PREDICT}"
|
|
282
|
+
if model:
|
|
283
|
+
endpoint += f"?model={model}"
|
|
284
|
+
|
|
285
|
+
body: dict[str, Any] = {
|
|
286
|
+
"question": question,
|
|
287
|
+
"user_context": user_context,
|
|
288
|
+
"user_id": "USER",
|
|
289
|
+
}
|
|
290
|
+
if prompt:
|
|
291
|
+
body["prompt"] = prompt
|
|
292
|
+
if context:
|
|
293
|
+
body["context"] = [
|
|
294
|
+
c.model_dump(mode="json") if isinstance(c, BaseModel) else c
|
|
295
|
+
for c in context
|
|
296
|
+
]
|
|
297
|
+
return await self._request(
|
|
298
|
+
"POST",
|
|
299
|
+
endpoint,
|
|
300
|
+
payload=body,
|
|
301
|
+
output=RephraseModel,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
async def remi(self, request: RemiRequest) -> RemiResponse:
|
|
305
|
+
endpoint = f"{self.url}{REMI_PREDICT}"
|
|
306
|
+
return await self._request(
|
|
307
|
+
"POST",
|
|
308
|
+
endpoint,
|
|
309
|
+
payload=request.model_dump(),
|
|
310
|
+
output=RemiResponse,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
async def generate_retrieval(
|
|
314
|
+
self,
|
|
315
|
+
question: str,
|
|
316
|
+
context: list[str],
|
|
317
|
+
model: Optional[str] = None,
|
|
318
|
+
) -> ChatResponse:
|
|
319
|
+
endpoint = f"{self.url}{CHAT_PREDICT}"
|
|
320
|
+
if model:
|
|
321
|
+
endpoint += f"?model={model}"
|
|
322
|
+
body = ChatModel(
|
|
323
|
+
question=question,
|
|
324
|
+
retrieval=True,
|
|
325
|
+
user_id="Nuclia PY CLI",
|
|
326
|
+
query_context=context,
|
|
327
|
+
)
|
|
328
|
+
return await self._request(
|
|
329
|
+
"POST", endpoint, payload=body.model_dump(), output=ChatResponse
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
async def rerank(self, model: RerankModel) -> RerankResponse:
|
|
333
|
+
endpoint = f"{self.url}{RERANK}"
|
|
334
|
+
return await self._request(
|
|
335
|
+
"POST", endpoint, payload=model.model_dump(), output=RerankResponse
|
|
336
|
+
)
|
hyperforge/openapi.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import datetime
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, FastAPI
|
|
6
|
+
from fastapi.openapi.utils import get_openapi
|
|
7
|
+
from starlette.routing import compile_path
|
|
8
|
+
|
|
9
|
+
extract_openapi_parser = argparse.ArgumentParser()
|
|
10
|
+
extract_openapi_parser.add_argument("openapi_json_path", type=str)
|
|
11
|
+
extract_openapi_parser.add_argument("api_version")
|
|
12
|
+
extract_openapi_parser.add_argument("commit_id", type=str)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def extract_openapi_command(component_id: str, title: str, router: APIRouter):
|
|
16
|
+
"""
|
|
17
|
+
This function assumes that json, api version and commit id are coming from the command line.
|
|
18
|
+
|
|
19
|
+
However, what can be wired in here is the title of the API and the API Router
|
|
20
|
+
that provides the endpoints.
|
|
21
|
+
|
|
22
|
+
This function assumes that you want to extract things from a router in the form of:
|
|
23
|
+
- /api/v1 or /api/v2
|
|
24
|
+
"""
|
|
25
|
+
args = extract_openapi_parser.parse_args()
|
|
26
|
+
openapi_json_path = args.openapi_json_path
|
|
27
|
+
api_version = args.api_version
|
|
28
|
+
commit_id = args.commit_id
|
|
29
|
+
|
|
30
|
+
app = FastAPI(title=title, version=f"{api_version}.0.0")
|
|
31
|
+
|
|
32
|
+
route_prefix = f"/api/v{api_version}"
|
|
33
|
+
routes = []
|
|
34
|
+
for route in router.routes:
|
|
35
|
+
# check if route starts with prefix and strip it, then add to new router
|
|
36
|
+
if route.path.startswith(route_prefix): # type: ignore
|
|
37
|
+
route.path = route.path[len(route_prefix) :] # type: ignore
|
|
38
|
+
route.path_regex, route.path_format, route.param_convertors = compile_path( # type: ignore
|
|
39
|
+
route.path # type: ignore
|
|
40
|
+
)
|
|
41
|
+
routes.append(route)
|
|
42
|
+
|
|
43
|
+
document = get_openapi(
|
|
44
|
+
title=app.title,
|
|
45
|
+
version=app.version,
|
|
46
|
+
openapi_version=app.openapi_version,
|
|
47
|
+
description=app.description,
|
|
48
|
+
terms_of_service=app.terms_of_service,
|
|
49
|
+
contact=app.contact,
|
|
50
|
+
license_info=app.license_info,
|
|
51
|
+
routes=routes,
|
|
52
|
+
tags=app.openapi_tags,
|
|
53
|
+
servers=app.servers,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
document["x-metadata"] = {
|
|
57
|
+
component_id: {
|
|
58
|
+
"commit": commit_id,
|
|
59
|
+
"last_updated": datetime.datetime.utcnow().isoformat(),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
json.dump(document, open(openapi_json_path, "w"))
|
hyperforge/prompts.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
from hyperforge import PROMPT_ENVIRONMENT
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PromptArgument(BaseModel):
|
|
7
|
+
"""An argument for a prompt template."""
|
|
8
|
+
|
|
9
|
+
name: str
|
|
10
|
+
"""The name of the argument."""
|
|
11
|
+
description: str | None = None
|
|
12
|
+
"""A human-readable description of the argument."""
|
|
13
|
+
required: bool | None = None
|
|
14
|
+
"""Whether this argument must be provided."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PromptConfig(BaseModel):
|
|
18
|
+
name: str
|
|
19
|
+
description: str
|
|
20
|
+
prompt: str
|
|
21
|
+
arguments: list[PromptArgument] | None = None
|
|
22
|
+
icons: dict[str, str] | None = None
|
|
23
|
+
meta: dict[str, str] | None = None
|
|
24
|
+
prompt_id: str | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
VALIDATE_OR_ANSWER_PROMPT = """
|
|
28
|
+
Based on the provided context and user question, perform the following tasks:
|
|
29
|
+
|
|
30
|
+
1. Select only information directly relevant to the question.
|
|
31
|
+
2. Break down compound sentences into simple, single-idea statements. Preserve original phrasing when possible.
|
|
32
|
+
3. For any named entity with descriptive details, separate those details into distinct propositions.
|
|
33
|
+
4. Ensure clarity by replacing pronouns (e.g., "it", "he", "she", "they", "this", "that") with the full names of the entities they reference, and add necessary modifiers to clarify meaning.
|
|
34
|
+
5. The context may be delimited by tags such as <START OF CONTEXT> and <END OF CONTEXT>. Treat everything between these tags as context.
|
|
35
|
+
6. Assess whether the context sufficiently answers the question. If it answers it partially, provide the answer; if it does not answer it fully, specify what information is missing to answer the question.
|
|
36
|
+
7. If the context does not answer the question at all, just return the original question as the missing information.
|
|
37
|
+
8. The `citations` field consists ONLY in a list of block IDs that are relevant to the answer, following these rules:
|
|
38
|
+
- Use the format: block-AB
|
|
39
|
+
- Just mention the block IDs, do NOT include any other text.
|
|
40
|
+
- Just mention the blocks actually relevant and that contain information used in the answer, do NOT include blocks that are not relevant.
|
|
41
|
+
- No duplicates.
|
|
42
|
+
9. Do NOT hallucinate block IDs. Only use those provided in the context.
|
|
43
|
+
10. Your output must be a JSON object with the following fields:
|
|
44
|
+
- "reason": Explain your reasoning for the answer or validation.
|
|
45
|
+
- "answer": Provide a partial or complete answer to the user query strictly from the information in the context. If there isn't enough information to even provide a partial answer, leave 'answer' empty.
|
|
46
|
+
- "missing_info_query": If the context is insufficient, specify what information is missing in a query shape; otherwise, leave it empty. Just return the query needed to retrieve the missing information.
|
|
47
|
+
- "useful": Indicate if the context is useful to answer the question ("yes" or "no").
|
|
48
|
+
- "citations": List the IDs of the blocks relevant to the answer, if any (e.g., ["block-AB", "block-CD"]).
|
|
49
|
+
11. **IMPORTANT** If any extra instructions are provided, you MUST follow them carefully when generating the answer field. These instructions may specify the format, style, tools to use, or other requirements for the answer.
|
|
50
|
+
|
|
51
|
+
{% if extra_prompts -%}
|
|
52
|
+
<ADDITIONAL INSTRUCTIONS>
|
|
53
|
+
These are extra instructions about how to generate the answer. They may include information about tools, style, or specific requirements.
|
|
54
|
+
**IMPORTANT** You MUST use these instructions when generating the answer, and follow any specific requirements they contain.
|
|
55
|
+
{% for prompt in extra_prompts -%}
|
|
56
|
+
- {{ prompt }}
|
|
57
|
+
{% endfor %}
|
|
58
|
+
<END OF ADDITIONAL INSTRUCTIONS>
|
|
59
|
+
{% endif -%}
|
|
60
|
+
|
|
61
|
+
<QUESTION>
|
|
62
|
+
{{question}}
|
|
63
|
+
|
|
64
|
+
<START OF CONTEXT>
|
|
65
|
+
{% if contexts %}
|
|
66
|
+
Context:
|
|
67
|
+
{% for ident, chunk in contexts.items() %}
|
|
68
|
+
**{{ident}}**\n\n{{chunk}}\n\n---"
|
|
69
|
+
|
|
70
|
+
{% endfor %}
|
|
71
|
+
{% endif %}
|
|
72
|
+
|
|
73
|
+
<END OF CONTEXT>
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
VALIDATE_OR_ANSWER_PROMPT_TEMPLATE = PROMPT_ENVIRONMENT.from_string(
|
|
79
|
+
VALIDATE_OR_ANSWER_PROMPT
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
VALIDATE_JSON_SCHEMA = {
|
|
84
|
+
"title": "validate_or_answer",
|
|
85
|
+
"description": "Validate or answer",
|
|
86
|
+
"parameters": {
|
|
87
|
+
"type": "object",
|
|
88
|
+
"properties": {
|
|
89
|
+
"reason": {
|
|
90
|
+
"type": "string",
|
|
91
|
+
"description": "Reasoning for the answer or validation",
|
|
92
|
+
},
|
|
93
|
+
"answer": {
|
|
94
|
+
"type": "string",
|
|
95
|
+
"description": "Partial or complete answer to the user query from the information in the context.",
|
|
96
|
+
},
|
|
97
|
+
"missing_info_query": {
|
|
98
|
+
"type": "string",
|
|
99
|
+
"description": "Query needed to retrieve the missing information in case the context is not enough to answer the question. If the context does not answer the question at all, just return the original question.",
|
|
100
|
+
},
|
|
101
|
+
"useful": {
|
|
102
|
+
"type": "string",
|
|
103
|
+
"description": "Is the context useful to answer the question?",
|
|
104
|
+
"enum": ["yes", "no"],
|
|
105
|
+
},
|
|
106
|
+
"citations": {
|
|
107
|
+
"type": "array",
|
|
108
|
+
"items": {
|
|
109
|
+
"type": "string",
|
|
110
|
+
"description": "Block ID cited in the answer, e.g. block-AB",
|
|
111
|
+
},
|
|
112
|
+
"description": "List of block IDs cited in the answer, if any",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
"required": ["reason", "answer", "missing_info_query", "useful", "citations"],
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
NEXT_REPHRASE_JSON_SCHEMA = {
|
|
121
|
+
"title": "next_rephrase",
|
|
122
|
+
"description": "Rephrase the question if needed given info about previous contexts and the current agent",
|
|
123
|
+
"parameters": {
|
|
124
|
+
"type": "object",
|
|
125
|
+
"properties": {
|
|
126
|
+
"rephrased_question": {
|
|
127
|
+
"type": "string",
|
|
128
|
+
"description": "Rephrased question if needed. If the question does not need to be rephrased, return an empty string.",
|
|
129
|
+
},
|
|
130
|
+
"needed": {
|
|
131
|
+
"type": "boolean",
|
|
132
|
+
"description": "Is rephrasing needed?",
|
|
133
|
+
},
|
|
134
|
+
"reason": {
|
|
135
|
+
"type": "string",
|
|
136
|
+
"description": "Reasoning for rephrasing or not the question",
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
"required": ["rephrased_question", "needed", "reason"],
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
NEXT_REPHRASE_PROMPT_SYSTEM = """
|
|
144
|
+
You decide if a question needs to be rephrased to be answered by the current agent, given the context provided by previous agents and the description of the current agent."""
|
|
145
|
+
|
|
146
|
+
NEXT_REPHRASE_PROMPT = """
|
|
147
|
+
You decide if a question needs to be rephrased to be answered by the current agent, given the context provided by previous agents and the description of the current agent.
|
|
148
|
+
|
|
149
|
+
Guidelines:
|
|
150
|
+
1. If the question can be answered by the current agent without rephrasing, return an empty string as the rephrased question and "False" as needed.
|
|
151
|
+
2. If the question needs to be rephrased to be answered by the current agent, return the rephrased question and "True" as needed.
|
|
152
|
+
3. Provide a clear reasoning for your decision in the reason field.
|
|
153
|
+
|
|
154
|
+
Return only a JSON object with the following fields:
|
|
155
|
+
- "rephrased_question": The rephrased question if needed. If the question does not need to be rephrased, return an empty string.
|
|
156
|
+
- "needed": "True" if rephrasing is needed, "False" otherwise.
|
|
157
|
+
- "reason": Explain your reasoning for rephrasing or not the question.
|
|
158
|
+
|
|
159
|
+
Here you have the question, the contexts from previous agents and the description of the current agent:
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
<CURRENT AGENT DESCRIPTION>
|
|
163
|
+
{{info}}
|
|
164
|
+
|
|
165
|
+
{%if extra_info %}
|
|
166
|
+
<ADDITIONAL INSTRUCTIONS>
|
|
167
|
+
These are extra instructions about how to rephrase the question. They may include information about format, style, tools to use, or specific requirements.
|
|
168
|
+
**IMPORTANT** You MUST use these instructions when rephrasing the question, and follow any specific requirements they contain.
|
|
169
|
+
{{extra_info}}
|
|
170
|
+
<END OF ADDITIONAL INSTRUCTIONS>
|
|
171
|
+
{% endif %}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
<PREVIOUS CONTEXTS>
|
|
175
|
+
{% if contexts %}
|
|
176
|
+
Context:
|
|
177
|
+
{% for context in contexts %}
|
|
178
|
+
- {{context}}
|
|
179
|
+
{% endfor %}
|
|
180
|
+
{% else %}
|
|
181
|
+
No previous context.
|
|
182
|
+
{% endif %}
|
|
183
|
+
|
|
184
|
+
<QUESTION>
|
|
185
|
+
{{question}}
|
|
186
|
+
|
|
187
|
+
"""
|
|
188
|
+
NEXT_REPHRASE_PROMPT_TEMPLATE = PROMPT_ENVIRONMENT.from_string(NEXT_REPHRASE_PROMPT)
|
hyperforge/pubsub.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from typing import Annotated, Dict, Literal
|
|
2
|
+
|
|
3
|
+
from pydantic import AliasChoices, BaseModel, Field
|
|
4
|
+
from pydantic.types import Discriminator, Tag
|
|
5
|
+
|
|
6
|
+
from hyperforge.interaction import (
|
|
7
|
+
AragAnswer,
|
|
8
|
+
Feedback,
|
|
9
|
+
OAuthAuthenticateURL,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
# Messages used in the pubsub protocol between API and agent servers
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StartInteraction(BaseModel):
|
|
16
|
+
"""Starts an interaction for a given agent/session"""
|
|
17
|
+
|
|
18
|
+
account: str
|
|
19
|
+
agent_id: str = Field(validation_alias=AliasChoices("agent_id", "kbid"))
|
|
20
|
+
session: str
|
|
21
|
+
question_id: str
|
|
22
|
+
question: str
|
|
23
|
+
headers: Dict[str, str] = {}
|
|
24
|
+
arguments: Dict[str, str] = {}
|
|
25
|
+
workflow_id: str = "default"
|
|
26
|
+
streaming: bool = False
|
|
27
|
+
op: Literal["start"] = "start"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class UserToAgentInteraction(BaseModel):
|
|
31
|
+
"""Sends an user response"""
|
|
32
|
+
|
|
33
|
+
op: Literal["user_response"] = "user_response"
|
|
34
|
+
request_id: str
|
|
35
|
+
response: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AgentPing(BaseModel):
|
|
39
|
+
"""Periodic ping to indicate the agent is still working"""
|
|
40
|
+
|
|
41
|
+
op: Literal["ping"] = "ping"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AgentDone(BaseModel):
|
|
45
|
+
"""Indicates the agent has finished and no more messages will be sent"""
|
|
46
|
+
|
|
47
|
+
op: Literal["done"] = "done"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AgentToUserRequest(BaseModel):
|
|
51
|
+
"""Sends a partial or final answer"""
|
|
52
|
+
|
|
53
|
+
op: Literal["agent_request"] = "agent_request"
|
|
54
|
+
feedback: Feedback
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class OAuthRequest(BaseModel):
|
|
58
|
+
"""Sends a partial or final answer"""
|
|
59
|
+
|
|
60
|
+
op: Literal["oauth"] = "oauth"
|
|
61
|
+
oauth: OAuthAuthenticateURL
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AgentAnswer(BaseModel):
|
|
65
|
+
"""Sends a partial or final answer"""
|
|
66
|
+
|
|
67
|
+
op: Literal["answer"] = "answer"
|
|
68
|
+
answer: AragAnswer
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Messages send from the agent server to the API server, to be passed to the client
|
|
72
|
+
AgentMessage = Annotated[
|
|
73
|
+
Annotated[AgentPing, Tag("ping")]
|
|
74
|
+
| Annotated[AgentDone, Tag("done")]
|
|
75
|
+
| Annotated[OAuthRequest, Tag("oauth")]
|
|
76
|
+
| Annotated[AgentAnswer, Tag("answer")]
|
|
77
|
+
| Annotated[AgentToUserRequest, Tag("agent_request")]
|
|
78
|
+
| Annotated[UserToAgentInteraction, Tag("user_response")],
|
|
79
|
+
Discriminator("op"),
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class QuitRequest(BaseModel):
|
|
84
|
+
"""Requests the agent to stop (the client has abandoned the session)"""
|
|
85
|
+
|
|
86
|
+
op: Literal["quit"] = "quit"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# Messages sent from the API to the agent server
|
|
90
|
+
APIMessage = Annotated[Annotated[QuitRequest, Tag("quit)")], Discriminator("op")]
|
hyperforge/py.typed
ADDED
|
File without changes
|