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.
Files changed (90) hide show
  1. hyperforge/__init__.py +16 -0
  2. hyperforge/agent.py +81 -0
  3. hyperforge/api/__init__.py +20 -0
  4. hyperforge/api/app.py +155 -0
  5. hyperforge/api/authentication.py +271 -0
  6. hyperforge/api/commands.py +33 -0
  7. hyperforge/api/internal/__init__.py +4 -0
  8. hyperforge/api/internal/inspect.py +30 -0
  9. hyperforge/api/internal/router.py +3 -0
  10. hyperforge/api/logging.py +18 -0
  11. hyperforge/api/models.py +129 -0
  12. hyperforge/api/session.py +197 -0
  13. hyperforge/api/settings.py +38 -0
  14. hyperforge/api/utils.py +354 -0
  15. hyperforge/api/v1/__init__.py +23 -0
  16. hyperforge/api/v1/agents.py +531 -0
  17. hyperforge/api/v1/interaction.py +430 -0
  18. hyperforge/api/v1/mcp_content.py +311 -0
  19. hyperforge/api/v1/mcp_interaction.py +322 -0
  20. hyperforge/api/v1/oauth.py +60 -0
  21. hyperforge/api/v1/prompt.py +129 -0
  22. hyperforge/api/v1/router.py +3 -0
  23. hyperforge/api/v1/schema.py +56 -0
  24. hyperforge/api/v1/session.py +182 -0
  25. hyperforge/api/v1/utils.py +12 -0
  26. hyperforge/api/v1/workflows.py +643 -0
  27. hyperforge/arag.py +28 -0
  28. hyperforge/broker/__init__.py +52 -0
  29. hyperforge/broker/local.py +116 -0
  30. hyperforge/broker/redis.py +161 -0
  31. hyperforge/configure.py +571 -0
  32. hyperforge/context/__init__.py +0 -0
  33. hyperforge/context/agent.py +377 -0
  34. hyperforge/context/config.py +103 -0
  35. hyperforge/database.py +3 -0
  36. hyperforge/db/__init__.py +6 -0
  37. hyperforge/db/agents.py +1521 -0
  38. hyperforge/db/encryption.py +91 -0
  39. hyperforge/db/exceptions.py +26 -0
  40. hyperforge/db/settings.py +16 -0
  41. hyperforge/db/workflow_cleanup.py +69 -0
  42. hyperforge/definition.py +13 -0
  43. hyperforge/driver.py +31 -0
  44. hyperforge/dummy.py +28 -0
  45. hyperforge/engine.py +189 -0
  46. hyperforge/exceptions.py +14 -0
  47. hyperforge/feature_flag.py +105 -0
  48. hyperforge/fixtures.py +602 -0
  49. hyperforge/interaction.py +116 -0
  50. hyperforge/llm.py +75 -0
  51. hyperforge/manager.py +432 -0
  52. hyperforge/memory/__init__.py +5 -0
  53. hyperforge/memory/memory.py +974 -0
  54. hyperforge/minimal_fixtures.py +75 -0
  55. hyperforge/models.py +336 -0
  56. hyperforge/nua.py +336 -0
  57. hyperforge/openapi.py +63 -0
  58. hyperforge/prompts.py +188 -0
  59. hyperforge/pubsub.py +90 -0
  60. hyperforge/py.typed +0 -0
  61. hyperforge/redis_utils.py +82 -0
  62. hyperforge/retrieval/__init__.py +0 -0
  63. hyperforge/retrieval/agent.py +169 -0
  64. hyperforge/retrieval/config.py +94 -0
  65. hyperforge/server/__init__.py +5 -0
  66. hyperforge/server/cache.py +131 -0
  67. hyperforge/server/run.py +109 -0
  68. hyperforge/server/sandbox.py +60 -0
  69. hyperforge/server/session.py +421 -0
  70. hyperforge/server/settings.py +47 -0
  71. hyperforge/server/utils.py +57 -0
  72. hyperforge/server/web.py +31 -0
  73. hyperforge/settings.py +18 -0
  74. hyperforge/standalone/__init__.py +5 -0
  75. hyperforge/standalone/agent.py +189 -0
  76. hyperforge/standalone/app.py +264 -0
  77. hyperforge/standalone/config.py +137 -0
  78. hyperforge/standalone/const.py +1 -0
  79. hyperforge/standalone/run.py +60 -0
  80. hyperforge/standalone/settings.py +133 -0
  81. hyperforge/standalone/ui_router.py +241 -0
  82. hyperforge/trace.py +42 -0
  83. hyperforge/utils/__init__.py +112 -0
  84. hyperforge/utils/http.py +48 -0
  85. hyperforge/workflows.py +44 -0
  86. hyperforge-1.0.0.post19.dist-info/METADATA +95 -0
  87. hyperforge-1.0.0.post19.dist-info/RECORD +90 -0
  88. hyperforge-1.0.0.post19.dist-info/WHEEL +5 -0
  89. hyperforge-1.0.0.post19.dist-info/entry_points.txt +8 -0
  90. 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