hyperforge-google 1.0.0.post29__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,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: hyperforge_google
3
+ Version: 1.0.0.post29
4
+ Summary: Google 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: google-genai
19
+
20
+ # Google Hyperforge agents
@@ -0,0 +1 @@
1
+ # Google Hyperforge agents
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "hyperforge_google"
7
+ version = "1.0.0.post29"
8
+ license = "Apache-2.0"
9
+ description = "Google 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 = ["hyperforge", "google-genai"]
22
+
23
+ [dependency-groups]
24
+ dev = [
25
+ "mypy",
26
+ "pytest",
27
+ "pytest-asyncio",
28
+ "pytest-recording",
29
+ "ty",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://progress.com"
34
+ Repository = "https://github.com/nuclia/hyperforge"
35
+
36
+ [tool.pytest.ini_options]
37
+ asyncio_mode = "auto"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ from .agent import GoogleAgent
2
+ from .driver import GoogleDriver
3
+
4
+ __all__ = ["GoogleAgent", "GoogleDriver"]
@@ -0,0 +1,199 @@
1
+ # GOOD REFERENCE: https://github.com/philschmid/gemini-samples/blob/main/examples/gemini-google-search.ipynb
2
+
3
+ from time import time
4
+ from typing import Any, ClassVar, Dict, List, Optional, Tuple, cast
5
+ from uuid import uuid4
6
+
7
+ from google.genai.types import (
8
+ GenerateContentConfig,
9
+ GoogleSearch,
10
+ ThinkingConfig,
11
+ Tool,
12
+ )
13
+ from hyperforge.agent import Agent
14
+ from hyperforge.configure import agent
15
+ from hyperforge.context.agent import ContextAgent
16
+ from hyperforge.definition import FunctionDefinition
17
+ from hyperforge.manager import Manager
18
+ from hyperforge.memory.memory import Chunk, Context, QuestionMemory
19
+ from hyperforge.utils.http import safe_http_client
20
+
21
+ from hyperforge_google.config import GoogleAgentConfig
22
+ from hyperforge_google.driver import GoogleDriver
23
+
24
+
25
+ @agent(
26
+ id="google",
27
+ agent_type="context",
28
+ title="Google Search",
29
+ description="Use Google Search to get information from the internet to answer questions.",
30
+ config_schema=GoogleAgentConfig,
31
+ )
32
+ class GoogleAgent(ContextAgent, Agent[GoogleAgentConfig]):
33
+ __published_functions__: ClassVar[Dict[str, FunctionDefinition]] = {
34
+ "internet_search": FunctionDefinition(
35
+ name="internet_search",
36
+ description="Performs an internet search using Google Search to get context to answer questions.",
37
+ parameters={
38
+ "question": {
39
+ "type": "string",
40
+ "description": "The question to search for on the internet.",
41
+ },
42
+ },
43
+ )
44
+ }
45
+ google_search_tool: Optional[Tool] = None
46
+
47
+ async def inner_from_config(
48
+ self, config: GoogleAgentConfig, agent_id: Optional[str] = None
49
+ ):
50
+ await self.context_from_config(config)
51
+ self.google_search_tool = Tool(google_search=GoogleSearch())
52
+
53
+ def driver(self, manager: Manager) -> GoogleDriver:
54
+ return cast(GoogleDriver, manager.drivers[self.config.source])
55
+
56
+ async def internet_search(
57
+ self,
58
+ question: str,
59
+ memory: QuestionMemory,
60
+ manager: Manager,
61
+ question_uuid: Optional[str] = None,
62
+ flow_id: Optional[str] = None,
63
+ ) -> Context:
64
+ if question_uuid is None:
65
+ question_uuid = uuid4().hex
66
+
67
+ driver = self.driver(manager)
68
+ t0 = time()
69
+ assert self.google_search_tool is not None, "google_search_tool not initialized"
70
+ response = await driver.client.aio.models.generate_content(
71
+ model=self.config.gen_model_id,
72
+ contents=question,
73
+ config=GenerateContentConfig(
74
+ tools=[self.google_search_tool],
75
+ response_modalities=["TEXT"],
76
+ thinking_config=ThinkingConfig(include_thoughts=True),
77
+ ),
78
+ )
79
+ if (
80
+ response is not None
81
+ and response.usage_metadata is not None
82
+ and response.usage_metadata.total_token_count is not None
83
+ and response.usage_metadata.prompt_token_count is not None
84
+ ):
85
+ tokens = response.usage_metadata.total_token_count * 0.05
86
+ input_tokens = response.usage_metadata.prompt_token_count * 0.05
87
+ output_tokens = tokens - input_tokens
88
+ else:
89
+ input_tokens = 0.5
90
+ output_tokens = 0.5
91
+ context = Context(
92
+ agent_id=self.config.id or self.agent_id,
93
+ original_question_uuid=memory.original_question_uuid,
94
+ actual_question_uuid=question_uuid,
95
+ question=question,
96
+ source="google",
97
+ agent="google",
98
+ title=self.config.title
99
+ if self.config.title
100
+ else "Internet search with Google",
101
+ )
102
+ http_client = safe_http_client()
103
+ chunks: Dict[int, str] = {}
104
+ answer = ""
105
+ reasoning = ""
106
+ count = 0
107
+ if (
108
+ response
109
+ and response.candidates
110
+ and len(response.candidates) > 0
111
+ and response.candidates[0].content
112
+ and response.candidates[0].content.parts
113
+ ):
114
+ candidate = response.candidates[0]
115
+ for each in response.candidates[0].content.parts:
116
+ if each.thought is True and each.text is not None:
117
+ reasoning += each.text
118
+ elif each.text is not None:
119
+ answer += each.text
120
+ if (
121
+ candidate.grounding_metadata
122
+ and candidate.grounding_metadata.grounding_chunks
123
+ ):
124
+ for chunk_id, chunk in enumerate(
125
+ candidate.grounding_metadata.grounding_chunks
126
+ ):
127
+ if chunk.web is not None and chunk.web.uri is not None:
128
+ resp = await http_client.get(chunk.web.uri)
129
+ chunks[chunk_id] = resp.headers.get("location", "")
130
+
131
+ if (
132
+ candidate.grounding_metadata
133
+ and candidate.grounding_metadata.grounding_supports
134
+ ):
135
+ for ground in candidate.grounding_metadata.grounding_supports:
136
+ if ground.grounding_chunk_indices:
137
+ url = [
138
+ chunks[indice] for indice in ground.grounding_chunk_indices
139
+ ]
140
+ else:
141
+ url = []
142
+ if ground.segment and ground.segment.text:
143
+ text = ground.segment.text
144
+ else:
145
+ text = ""
146
+ count += 1
147
+ context.chunks.append(
148
+ Chunk(
149
+ chunk_id=uuid4().hex,
150
+ text=text,
151
+ labels=[],
152
+ url=url,
153
+ origin_agent=self.config.module,
154
+ )
155
+ )
156
+ # if candidate.grounding_metadata.web_search_queries:
157
+ # for query in candidate.grounding_metadata.web_search_queries:
158
+ # # Queries
159
+ await memory.add_answer(
160
+ answer,
161
+ module=self.config.module,
162
+ agent_path=f"/context/{self.config.id if self.config.id else 'default'}",
163
+ )
164
+ context.summary = answer
165
+ await memory.add_step(
166
+ step_module=self.config.module,
167
+ step_title=self.step_title("Search results"),
168
+ step_agent_path=f"/context/{self.config.id if self.config.id else 'default'}",
169
+ step_reason=reasoning,
170
+ step_value=f"{count} results found",
171
+ timeit=time() - t0,
172
+ input_nuclia_tokens=input_tokens,
173
+ output_nuclia_tokens=output_tokens,
174
+ )
175
+ return context
176
+
177
+ async def _get_question_context(
178
+ self,
179
+ memory: QuestionMemory,
180
+ manager: Manager,
181
+ question_uuid: str,
182
+ question: str,
183
+ flow_id: str,
184
+ extra_context: Optional[Dict[str, Any]] = None,
185
+ ) -> List[Tuple[str, str]]:
186
+ context = await self.internet_search(
187
+ question=question,
188
+ memory=memory,
189
+ manager=manager,
190
+ question_uuid=question_uuid,
191
+ )
192
+ missing = await self.save_ctx_and_return_missing(
193
+ context=context,
194
+ question=question,
195
+ memory=memory,
196
+ manager=manager,
197
+ flow_id=flow_id,
198
+ )
199
+ return [missing] if missing is not None else []
@@ -0,0 +1,38 @@
1
+ from typing import ClassVar, Literal, Optional, Tuple
2
+
3
+ from hyperforge.context.config import ContextAgentConfig
4
+ from hyperforge.driver import DriverConfig, EncryptedPayload
5
+ from hyperforge.utils import WidgetType
6
+ from pydantic import Field
7
+ from pydantic.config import ConfigDict
8
+
9
+
10
+ class GoogleInnerConfig(EncryptedPayload):
11
+ encrypted_fields: ClassVar[list[str]] = ["api_key", "credentials"]
12
+
13
+ api_key: Optional[str] = None
14
+ credentials: Optional[str] = None
15
+ vertexai: bool = False
16
+ project: Optional[str] = None
17
+ location: Optional[str] = None
18
+
19
+
20
+ class GoogleDriverConfig(DriverConfig[GoogleInnerConfig]):
21
+ model_config = ConfigDict(title="Google Gemini")
22
+ provider: Literal["google"] = "google"
23
+ config: GoogleInnerConfig
24
+
25
+
26
+ class GoogleAgentConfig(ContextAgentConfig):
27
+ model_config = ConfigDict(title="Google Gemini")
28
+ module: Literal["google"] = "google"
29
+ gen_model_id: str = "gemini-2.5-flash"
30
+ source: str = "google"
31
+ published_functions: Optional[Tuple[str, ...]] = Field(
32
+ default=("internet_search",),
33
+ title="Published functions",
34
+ description="List of functions published by this agent to be used by other agents in the chain",
35
+ json_schema_extra={
36
+ "widget": WidgetType.NOT_SHOWN,
37
+ },
38
+ )
@@ -0,0 +1,36 @@
1
+ from google.genai import Client
2
+ from hyperforge.configure import driver
3
+ from hyperforge.driver import Driver
4
+ from typing_extensions import Self
5
+
6
+ from hyperforge_google.config import GoogleDriverConfig
7
+
8
+
9
+ @driver(
10
+ id="google",
11
+ title="Google Driver",
12
+ description="Driver for interacting with the Google API.",
13
+ config_schema=GoogleDriverConfig,
14
+ )
15
+ class GoogleDriver(Driver):
16
+ client: Client
17
+
18
+ @classmethod
19
+ async def init(cls, driver: GoogleDriverConfig) -> Self:
20
+ creds = None
21
+ if driver.config.vertexai:
22
+ from google.auth import load_credentials_from_file
23
+
24
+ creds, _ = load_credentials_from_file(driver.config.credentials)
25
+ client = Client(
26
+ vertexai=driver.config.vertexai,
27
+ credentials=creds,
28
+ api_key=driver.config.api_key,
29
+ project=driver.config.project,
30
+ location=driver.config.location,
31
+ )
32
+ return cls(
33
+ client=client,
34
+ name=driver.name,
35
+ provider=driver.provider,
36
+ )
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: hyperforge_google
3
+ Version: 1.0.0.post29
4
+ Summary: Google 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: google-genai
19
+
20
+ # Google Hyperforge agents
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/hyperforge_google/__init__.py
4
+ src/hyperforge_google/agent.py
5
+ src/hyperforge_google/config.py
6
+ src/hyperforge_google/driver.py
7
+ src/hyperforge_google/py.typed
8
+ src/hyperforge_google.egg-info/PKG-INFO
9
+ src/hyperforge_google.egg-info/SOURCES.txt
10
+ src/hyperforge_google.egg-info/dependency_links.txt
11
+ src/hyperforge_google.egg-info/requires.txt
12
+ src/hyperforge_google.egg-info/top_level.txt
13
+ tests/test_google.py
@@ -0,0 +1,2 @@
1
+ hyperforge
2
+ google-genai
@@ -0,0 +1,91 @@
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_google.agent import GoogleAgent
12
+ from hyperforge_google.config import GoogleAgentConfig
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
+ pytestmark = [
20
+ pytest.mark.vcr(
21
+ ignore_localhost=True,
22
+ match_on=["scheme", "host", "port", "path"],
23
+ ),
24
+ pytest.mark.asyncio,
25
+ ]
26
+
27
+
28
+ DRIVERS = [
29
+ {
30
+ "provider": "google",
31
+ "identifier": "google-01",
32
+ "name": "google",
33
+ "config": {
34
+ "vertexai": False,
35
+ "api_key": os.environ.get("GOOGLE_API_KEY", "DUMMY_API_KEY"),
36
+ },
37
+ },
38
+ ]
39
+
40
+
41
+ async def _run_question(
42
+ drivers: list[dict],
43
+ question: str,
44
+ ) -> list[Context]:
45
+ """Build a manager from driver configs, run the agent, return contexts."""
46
+ load_all_configurations("hyperforge_google")
47
+ # Google agent doesn't use nua — construct client directly with test values
48
+ nua = AsyncNuaClient(region="europe-1", account="test", token=NUA_KEY)
49
+ manager = await Manager.from_config(
50
+ drivers=[get_driver_config_instance(d) for d in drivers],
51
+ nua=nua,
52
+ )
53
+ config = GoogleAgentConfig.model_validate(
54
+ {
55
+ "module": "google",
56
+ "title": "Google Agent",
57
+ "source": "google-01",
58
+ }
59
+ )
60
+ agent = GoogleAgent(config=config)
61
+ await agent.inner_from_config(config)
62
+
63
+ session = EphemeralSessionMemory.from_config(
64
+ config=MemoryConfig(),
65
+ agent_id="test",
66
+ workflow_id="test",
67
+ rules=Rules(),
68
+ )
69
+ session.init("test-session")
70
+ memory = session.start_question(question)
71
+
72
+ flow_id = uuid4().hex
73
+ await agent.get_question_context(
74
+ memory=memory,
75
+ manager=manager,
76
+ question_uuid=memory.original_question_uuid,
77
+ question=question,
78
+ flow_id=flow_id,
79
+ )
80
+ return memory.get_agent_contexts(flow_id=flow_id, agent_id=agent.agent_id)
81
+
82
+
83
+ async def test_google():
84
+ contexts = await _run_question(
85
+ DRIVERS,
86
+ "What is Nuclia?",
87
+ )
88
+ assert len(contexts) > 0
89
+ all_text = " ".join(chunk.text for ctx in contexts for chunk in ctx.chunks).lower()
90
+ summary = " ".join(ctx.summary for ctx in contexts if ctx.summary).lower()
91
+ assert "rag" in all_text or "ai" in all_text or "rag" in summary or "ai" in summary