hyperforge-perplexity-search 1.0.0.post20__tar.gz

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.
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: hyperforge_perplexity_search
3
+ Version: 1.0.0.post20
4
+ Summary: Perplexity Search Hyperforge agent
5
+ Author-email: Nuclia <nucliadb@nuclia.com>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://progress.com
8
+ Project-URL: Repository, https://github.com/nuclia/hyperforge
9
+ Classifier: Programming Language :: Python
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Requires-Python: <4,>=3.10
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: hyperforge
18
+ Requires-Dist: httpx
19
+ Requires-Dist: hyperforge_perplexity
20
+ Requires-Dist: perplexityai>=0.35.1
21
+
22
+ # Perplexity Search Hyperforge agents
@@ -0,0 +1 @@
1
+ # Perplexity Search Hyperforge agents
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "hyperforge_perplexity_search"
7
+ version = "1.0.0.post20"
8
+ license = "Apache-2.0"
9
+ description = "Perplexity Search Hyperforge agent"
10
+ authors = [{ name = "Nuclia", email = "nucliadb@nuclia.com" }]
11
+ readme = "README.md"
12
+ classifiers = [
13
+ "Programming Language :: Python",
14
+ "Programming Language :: Python :: 3.10",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3 :: Only",
18
+ "Topic :: Software Development :: Libraries :: Python Modules",
19
+ ]
20
+ requires-python = ">=3.10, <4"
21
+ dependencies = [
22
+ "hyperforge",
23
+ "httpx",
24
+ "hyperforge_perplexity",
25
+ "perplexityai>=0.35.1",
26
+ ]
27
+
28
+ [dependency-groups]
29
+ dev = [
30
+ "mypy",
31
+ "pytest",
32
+ "pytest-asyncio",
33
+ "pytest-recording",
34
+ "ty",
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://progress.com"
39
+ Repository = "https://github.com/nuclia/hyperforge"
40
+
41
+ [tool.pytest.ini_options]
42
+ asyncio_mode = "auto"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from .agent import PerplexitySearchAgent
2
+
3
+ __all__ = ["PerplexitySearchAgent"]
@@ -0,0 +1,140 @@
1
+ from time import time
2
+ from typing import Any, ClassVar, Dict, List, Optional, cast
3
+ from uuid import uuid4
4
+
5
+ from hyperforge.agent import Agent
6
+ from hyperforge.configure import agent
7
+ from hyperforge.context.agent import ContextAgent
8
+ from hyperforge.definition import FunctionDefinition
9
+ from hyperforge.manager import Manager
10
+ from hyperforge.memory import Chunk, Context, QuestionMemory
11
+ from hyperforge_perplexity.driver import PerplexityDriver
12
+
13
+ from hyperforge_perplexity_search.config import PerplexitySearchAgentConfig
14
+
15
+ SYSTEM_PROMPT = "Be precise and concise"
16
+
17
+
18
+ @agent(
19
+ id="perplexity_search",
20
+ agent_type="context",
21
+ title="Perplexity Search",
22
+ description="Use Perplexity to get information from the internet.",
23
+ config_schema=PerplexitySearchAgentConfig,
24
+ )
25
+ class PerplexitySearchAgent(ContextAgent, Agent[PerplexitySearchAgentConfig]):
26
+ driver: Optional[PerplexityDriver] = None
27
+ __published_functions__: ClassVar[Dict[str, FunctionDefinition]] = {
28
+ "internet_search": FunctionDefinition(
29
+ name="internet_search",
30
+ description="Performs an internet search using Perplexity and returns the results as context to answer questions. Does not generate an answer, only search and return the results as context.",
31
+ parameters={
32
+ "question": {
33
+ "type": "string",
34
+ "description": "The question to search for on the internet.",
35
+ },
36
+ },
37
+ )
38
+ }
39
+
40
+ async def search(
41
+ self,
42
+ question: str,
43
+ memory: QuestionMemory,
44
+ manager: Manager,
45
+ question_uuid: Optional[str] = None,
46
+ ) -> Context:
47
+ t0 = time()
48
+
49
+ if self.driver is None:
50
+ self.driver: Optional[PerplexityDriver] = cast(
51
+ Optional[PerplexityDriver], manager.drivers.get(self.config.source)
52
+ )
53
+ if self.driver is None:
54
+ raise Exception("Perplexity driver does not exist")
55
+
56
+ response = await self.driver.client.search.create(
57
+ query=question,
58
+ search_domain_filter=self.config.domain,
59
+ max_results=self.config.max_results,
60
+ max_tokens_per_page=self.config.max_tokens_per_page,
61
+ )
62
+ chunks = []
63
+ if response.results is not None:
64
+ for result in response.results:
65
+ text = result.snippet if result.snippet is not None else ""
66
+ title = (
67
+ result.title
68
+ if result.title is not None
69
+ else result.url
70
+ if result.url is not None
71
+ else "No title"
72
+ )
73
+ chunk = Chunk(
74
+ chunk_id=uuid4().hex,
75
+ text=text,
76
+ title=title,
77
+ labels=[],
78
+ url=[result.url] if result.url is not None else [],
79
+ origin_url=result.url if result.url is not None else "",
80
+ origin_agent=self.config.module,
81
+ )
82
+ chunks.append(chunk)
83
+ context = Context(
84
+ agent_id=self.config.id or self.agent_id,
85
+ original_question_uuid=memory.original_question_uuid,
86
+ actual_question_uuid=question_uuid,
87
+ question=question,
88
+ title=self.config.title
89
+ if self.config.title
90
+ else "Internet search with Perplexity",
91
+ source="perplexity",
92
+ agent=self.config.module,
93
+ chunks=chunks,
94
+ )
95
+ await memory.add_step(
96
+ step_module=self.config.module,
97
+ step_title=self.step_title("Search results"),
98
+ step_reason="",
99
+ step_agent_path=f"/context/{self.config.id or self.agent_id}",
100
+ step_value=f"{len(chunks)} results found with Perplexity"
101
+ if chunks
102
+ else "No results found",
103
+ timeit=time() - t0,
104
+ input_nuclia_tokens=0,
105
+ output_nuclia_tokens=0,
106
+ )
107
+ return context
108
+
109
+ async def _get_question_context(
110
+ self,
111
+ memory: QuestionMemory,
112
+ manager: Manager,
113
+ question_uuid: str,
114
+ question: str,
115
+ flow_id: str,
116
+ extra_context: Optional[Dict[str, Any]] = None,
117
+ ) -> List[tuple[str, str]]:
118
+ if self.driver is None:
119
+ self.driver: Optional[PerplexityDriver] = cast(
120
+ Optional[PerplexityDriver], manager.drivers.get(self.config.source)
121
+ )
122
+
123
+ if self.driver is None:
124
+ raise Exception("Perplexity driver does not exist")
125
+
126
+ context = await self.search(
127
+ question,
128
+ memory,
129
+ manager,
130
+ question_uuid=question_uuid,
131
+ )
132
+
133
+ missing = await self.save_ctx_and_return_missing(
134
+ context=context,
135
+ question=question,
136
+ memory=memory,
137
+ manager=manager,
138
+ flow_id=flow_id,
139
+ )
140
+ return [missing] if missing is not None else []
@@ -0,0 +1,35 @@
1
+ from typing import List, Literal
2
+
3
+ from hyperforge.context.config import ContextAgentConfig
4
+ from pydantic import Field
5
+ from pydantic.config import ConfigDict
6
+
7
+
8
+ class PerplexitySearchAgentConfig(ContextAgentConfig):
9
+ model_config = ConfigDict(title="Perplexity Search")
10
+ module: Literal["perplexity_search"] = "perplexity_search"
11
+ domain: List[str] = Field(
12
+ default_factory=list,
13
+ title="Domain Filter",
14
+ description="Domains to restrict the Perplexity search to.",
15
+ )
16
+ search_context_size: Literal["low", "medium", "high"] = Field(
17
+ default="low",
18
+ title="Search Context Size",
19
+ description="Determines how much search context Perplexity retrieves for the model. "
20
+ "Options are: `low` (minimizes context for cost savings but less comprehensive answers), "
21
+ "`medium` (balanced approach suitable for most queries), "
22
+ "and `high` (maximizes context for comprehensive answers but at higher cost).",
23
+ )
24
+ max_results: int = Field(
25
+ default=10,
26
+ title="Max Results",
27
+ description="Maximum number of search results to return.",
28
+ )
29
+ max_tokens_per_page: int = Field(
30
+ default=4096,
31
+ title="Max Tokens per Page",
32
+ description="Maximum number of tokens to return per search result page.",
33
+ )
34
+
35
+ source: str = "perplexity"
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: hyperforge_perplexity_search
3
+ Version: 1.0.0.post20
4
+ Summary: Perplexity Search Hyperforge agent
5
+ Author-email: Nuclia <nucliadb@nuclia.com>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://progress.com
8
+ Project-URL: Repository, https://github.com/nuclia/hyperforge
9
+ Classifier: Programming Language :: Python
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Requires-Python: <4,>=3.10
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: hyperforge
18
+ Requires-Dist: httpx
19
+ Requires-Dist: hyperforge_perplexity
20
+ Requires-Dist: perplexityai>=0.35.1
21
+
22
+ # Perplexity Search Hyperforge agents
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/hyperforge_perplexity_search/__init__.py
4
+ src/hyperforge_perplexity_search/agent.py
5
+ src/hyperforge_perplexity_search/config.py
6
+ src/hyperforge_perplexity_search/py.typed
7
+ src/hyperforge_perplexity_search.egg-info/PKG-INFO
8
+ src/hyperforge_perplexity_search.egg-info/SOURCES.txt
9
+ src/hyperforge_perplexity_search.egg-info/dependency_links.txt
10
+ src/hyperforge_perplexity_search.egg-info/requires.txt
11
+ src/hyperforge_perplexity_search.egg-info/top_level.txt
12
+ tests/test_perplexity_search.py
@@ -0,0 +1,4 @@
1
+ hyperforge
2
+ httpx
3
+ hyperforge_perplexity
4
+ perplexityai>=0.35.1
@@ -0,0 +1,90 @@
1
+ import os
2
+ from uuid import uuid4
3
+
4
+ import pytest
5
+ from hyperforge.configure import get_driver_config_instance, load_all_configurations
6
+ from hyperforge.manager import Manager
7
+ from hyperforge.memory import Context
8
+ from hyperforge.memory.memory import EphemeralSessionMemory
9
+ from hyperforge.minimal_fixtures import cassette_nua_key
10
+ from hyperforge.models import MemoryConfig, Rules
11
+ from hyperforge_perplexity_search.agent import PerplexitySearchAgent
12
+ from hyperforge_perplexity_search.config import PerplexitySearchAgentConfig
13
+ from nuclia.lib.nua import AsyncNuaClient
14
+
15
+ NUA_KEY = os.environ.get(
16
+ "NUA_KEY",
17
+ ) or cassette_nua_key("https://europe-1.nuclia.cloud/")
18
+
19
+ PERPLEXITY_KEY = os.environ.get("PERPLEXITY_API_KEY", "DUMMY_PERPLEXITY_KEY")
20
+
21
+ pytestmark = [
22
+ pytest.mark.vcr(ignore_localhost=True),
23
+ pytest.mark.asyncio,
24
+ ]
25
+
26
+ DRIVERS = [
27
+ {
28
+ "provider": "perplexity",
29
+ "identifier": "perplexity-01",
30
+ "name": "perplexity",
31
+ "config": {"key": PERPLEXITY_KEY},
32
+ },
33
+ ]
34
+
35
+
36
+ async def _run_question(
37
+ drivers: list[dict], question: str, config_overrides: dict | None = None
38
+ ) -> list[Context]:
39
+ load_all_configurations("hyperforge_perplexity_search")
40
+ load_all_configurations("hyperforge_perplexity") # register the perplexity driver
41
+ # Perplexity agent doesn't use nua — construct client directly with test values
42
+ nua = AsyncNuaClient(region="europe-1", account="test", token=NUA_KEY)
43
+ manager = await Manager.from_config(
44
+ drivers=[get_driver_config_instance(d) for d in drivers],
45
+ nua=nua,
46
+ )
47
+ config = PerplexitySearchAgentConfig.model_validate(
48
+ {
49
+ "module": "perplexity_search",
50
+ "title": "Perplexity Search Agent",
51
+ "source": "perplexity-01",
52
+ **(config_overrides or {}),
53
+ }
54
+ )
55
+ agent = PerplexitySearchAgent(config=config)
56
+ await agent.inner_from_config(config)
57
+
58
+ session = EphemeralSessionMemory.from_config(
59
+ config=MemoryConfig(), agent_id="test", workflow_id="test", rules=Rules()
60
+ )
61
+ session.init("test-session")
62
+ memory = session.start_question(question)
63
+ flow_id = uuid4().hex
64
+ await agent.get_question_context(
65
+ memory=memory,
66
+ manager=manager,
67
+ question_uuid=memory.original_question_uuid,
68
+ question=question,
69
+ flow_id=flow_id,
70
+ )
71
+ return memory.get_agent_contexts(flow_id=flow_id, agent_id=agent.agent_id)
72
+
73
+
74
+ async def test_perplexity_search():
75
+ contexts = await _run_question(
76
+ DRIVERS,
77
+ "What is Nuclia?",
78
+ config_overrides={"domain": ["nuclia.com"], "max_results": 3},
79
+ )
80
+ assert len(contexts) > 0
81
+ assert any(ctx.chunks for ctx in contexts)
82
+
83
+
84
+ async def test_perplexity_search_domain():
85
+ contexts = await _run_question(
86
+ DRIVERS,
87
+ "What is Marklogic?",
88
+ config_overrides={"domain": ["progress.com"], "max_results": 5},
89
+ )
90
+ assert len(contexts) > 0