vibesurf 0.1.10__py3-none-any.whl → 0.1.11__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.
Potentially problematic release.
This version of vibesurf might be problematic. Click here for more details.
- vibe_surf/_version.py +2 -2
- vibe_surf/agents/browser_use_agent.py +68 -45
- vibe_surf/agents/prompts/report_writer_prompt.py +73 -0
- vibe_surf/agents/prompts/vibe_surf_prompt.py +85 -172
- vibe_surf/agents/report_writer_agent.py +380 -226
- vibe_surf/agents/vibe_surf_agent.py +879 -825
- vibe_surf/agents/views.py +130 -0
- vibe_surf/backend/api/activity.py +3 -1
- vibe_surf/backend/api/browser.py +9 -5
- vibe_surf/backend/api/config.py +8 -5
- vibe_surf/backend/api/files.py +59 -50
- vibe_surf/backend/api/models.py +2 -2
- vibe_surf/backend/api/task.py +45 -12
- vibe_surf/backend/database/manager.py +24 -18
- vibe_surf/backend/database/queries.py +199 -192
- vibe_surf/backend/database/schemas.py +1 -1
- vibe_surf/backend/main.py +4 -2
- vibe_surf/backend/shared_state.py +28 -35
- vibe_surf/backend/utils/encryption.py +3 -1
- vibe_surf/backend/utils/llm_factory.py +41 -36
- vibe_surf/browser/agent_browser_session.py +0 -4
- vibe_surf/browser/browser_manager.py +14 -8
- vibe_surf/browser/utils.py +5 -3
- vibe_surf/browser/watchdogs/dom_watchdog.py +0 -45
- vibe_surf/chrome_extension/background.js +4 -0
- vibe_surf/chrome_extension/scripts/api-client.js +13 -0
- vibe_surf/chrome_extension/scripts/file-manager.js +27 -71
- vibe_surf/chrome_extension/scripts/session-manager.js +21 -3
- vibe_surf/chrome_extension/scripts/ui-manager.js +831 -48
- vibe_surf/chrome_extension/sidepanel.html +21 -4
- vibe_surf/chrome_extension/styles/activity.css +365 -5
- vibe_surf/chrome_extension/styles/input.css +139 -0
- vibe_surf/cli.py +4 -22
- vibe_surf/common.py +35 -0
- vibe_surf/llm/openai_compatible.py +148 -93
- vibe_surf/logger.py +99 -0
- vibe_surf/{controller/vibesurf_tools.py → tools/browser_use_tools.py} +233 -219
- vibe_surf/tools/file_system.py +415 -0
- vibe_surf/{controller → tools}/mcp_client.py +4 -3
- vibe_surf/tools/report_writer_tools.py +21 -0
- vibe_surf/tools/vibesurf_tools.py +657 -0
- vibe_surf/tools/views.py +120 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/METADATA +6 -2
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/RECORD +49 -43
- vibe_surf/controller/file_system.py +0 -53
- vibe_surf/controller/views.py +0 -37
- /vibe_surf/{controller → tools}/__init__.py +0 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/WHEEL +0 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/entry_points.txt +0 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/licenses/LICENSE +0 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/top_level.txt +0 -0
|
@@ -31,8 +31,25 @@ from pydantic import BaseModel
|
|
|
31
31
|
|
|
32
32
|
from browser_use.llm.openai.chat import ChatOpenAI
|
|
33
33
|
from browser_use.llm.messages import BaseMessage
|
|
34
|
+
from collections.abc import Iterable, Mapping
|
|
35
|
+
from dataclasses import dataclass, field
|
|
36
|
+
from typing import Any, Literal, TypeVar, overload
|
|
37
|
+
|
|
38
|
+
import httpx
|
|
39
|
+
from openai import APIConnectionError, APIStatusError, AsyncOpenAI, RateLimitError
|
|
40
|
+
from openai.types.chat import ChatCompletionContentPartTextParam
|
|
41
|
+
from openai.types.chat.chat_completion import ChatCompletion
|
|
42
|
+
from openai.types.shared.chat_model import ChatModel
|
|
43
|
+
from openai.types.shared_params.reasoning_effort import ReasoningEffort
|
|
44
|
+
from openai.types.shared_params.response_format_json_schema import JSONSchema, ResponseFormatJSONSchema
|
|
45
|
+
from pydantic import BaseModel
|
|
46
|
+
|
|
47
|
+
from browser_use.llm.base import BaseChatModel
|
|
48
|
+
from browser_use.llm.exceptions import ModelProviderError
|
|
49
|
+
from browser_use.llm.messages import BaseMessage
|
|
50
|
+
from browser_use.llm.openai.serializer import OpenAIMessageSerializer
|
|
34
51
|
from browser_use.llm.schema import SchemaOptimizer
|
|
35
|
-
from browser_use.llm.views import ChatInvokeCompletion
|
|
52
|
+
from browser_use.llm.views import ChatInvokeCompletion, ChatInvokeUsage
|
|
36
53
|
|
|
37
54
|
T = TypeVar('T', bound=BaseModel)
|
|
38
55
|
|
|
@@ -50,11 +67,11 @@ class ChatOpenAICompatible(ChatOpenAI):
|
|
|
50
67
|
like "Unable to submit request because one or more response schemas specified
|
|
51
68
|
other fields alongside any_of".
|
|
52
69
|
"""
|
|
53
|
-
|
|
70
|
+
|
|
54
71
|
def _is_gemini_model(self) -> bool:
|
|
55
72
|
"""Check if the current model is a Gemini model."""
|
|
56
73
|
return str(self.model).lower().startswith('gemini')
|
|
57
|
-
|
|
74
|
+
|
|
58
75
|
def _fix_gemini_schema(self, schema: dict[str, Any]) -> dict[str, Any]:
|
|
59
76
|
"""
|
|
60
77
|
Convert a Pydantic model to a Gemini-compatible schema.
|
|
@@ -64,11 +81,11 @@ class ChatOpenAICompatible(ChatOpenAI):
|
|
|
64
81
|
|
|
65
82
|
Adapted from browser_use.llm.google.chat.ChatGoogle._fix_gemini_schema
|
|
66
83
|
"""
|
|
67
|
-
|
|
84
|
+
|
|
68
85
|
# Handle $defs and $ref resolution
|
|
69
86
|
if '$defs' in schema:
|
|
70
87
|
defs = schema.pop('$defs')
|
|
71
|
-
|
|
88
|
+
|
|
72
89
|
def resolve_refs(obj: Any) -> Any:
|
|
73
90
|
if isinstance(obj, dict):
|
|
74
91
|
if '$ref' in obj:
|
|
@@ -89,9 +106,9 @@ class ChatOpenAICompatible(ChatOpenAI):
|
|
|
89
106
|
elif isinstance(obj, list):
|
|
90
107
|
return [resolve_refs(item) for item in obj]
|
|
91
108
|
return obj
|
|
92
|
-
|
|
109
|
+
|
|
93
110
|
schema = resolve_refs(schema)
|
|
94
|
-
|
|
111
|
+
|
|
95
112
|
# Remove unsupported properties
|
|
96
113
|
def clean_schema(obj: Any) -> Any:
|
|
97
114
|
if isinstance(obj, dict):
|
|
@@ -102,136 +119,174 @@ class ChatOpenAICompatible(ChatOpenAI):
|
|
|
102
119
|
cleaned_value = clean_schema(value)
|
|
103
120
|
# Handle empty object properties - Gemini doesn't allow empty OBJECT types
|
|
104
121
|
if (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
122
|
+
key == 'properties'
|
|
123
|
+
and isinstance(cleaned_value, dict)
|
|
124
|
+
and len(cleaned_value) == 0
|
|
125
|
+
and isinstance(obj.get('type', ''), str)
|
|
126
|
+
and obj.get('type', '').upper() == 'OBJECT'
|
|
110
127
|
):
|
|
111
128
|
# Convert empty object to have at least one property
|
|
112
129
|
cleaned['properties'] = {'_placeholder': {'type': 'string'}}
|
|
113
130
|
else:
|
|
114
131
|
cleaned[key] = cleaned_value
|
|
115
|
-
|
|
132
|
+
|
|
116
133
|
# If this is an object type with empty properties, add a placeholder
|
|
117
134
|
if (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
135
|
+
isinstance(cleaned.get('type', ''), str)
|
|
136
|
+
and cleaned.get('type', '').upper() == 'OBJECT'
|
|
137
|
+
and 'properties' in cleaned
|
|
138
|
+
and isinstance(cleaned['properties'], dict)
|
|
139
|
+
and len(cleaned['properties']) == 0
|
|
123
140
|
):
|
|
124
141
|
cleaned['properties'] = {'_placeholder': {'type': 'string'}}
|
|
125
|
-
|
|
142
|
+
|
|
126
143
|
return cleaned
|
|
127
144
|
elif isinstance(obj, list):
|
|
128
145
|
return [clean_schema(item) for item in obj]
|
|
129
146
|
return obj
|
|
130
|
-
|
|
147
|
+
|
|
131
148
|
return clean_schema(schema)
|
|
132
|
-
|
|
149
|
+
|
|
133
150
|
@overload
|
|
134
|
-
async def ainvoke(self, messages: list[BaseMessage], output_format: None = None) -> ChatInvokeCompletion[str]:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
151
|
+
async def ainvoke(self, messages: list[BaseMessage], output_format: None = None) -> ChatInvokeCompletion[str]:
|
|
152
|
+
...
|
|
153
|
+
|
|
154
|
+
@overload
|
|
155
|
+
async def ainvoke(self, messages: list[BaseMessage], output_format: type[T]) -> ChatInvokeCompletion[T]:
|
|
156
|
+
...
|
|
157
|
+
|
|
139
158
|
async def ainvoke(
|
|
140
|
-
|
|
159
|
+
self, messages: list[BaseMessage], output_format: type[T] | None = None
|
|
141
160
|
) -> ChatInvokeCompletion[T] | ChatInvokeCompletion[str]:
|
|
142
161
|
"""
|
|
143
162
|
Invoke the model with the given messages.
|
|
144
|
-
|
|
145
|
-
Automatically applies Gemini schema fixes when using Gemini models.
|
|
146
|
-
|
|
163
|
+
|
|
147
164
|
Args:
|
|
148
165
|
messages: List of chat messages
|
|
149
166
|
output_format: Optional Pydantic model class for structured output
|
|
150
|
-
|
|
167
|
+
|
|
151
168
|
Returns:
|
|
152
169
|
Either a string response or an instance of output_format
|
|
153
170
|
"""
|
|
154
|
-
|
|
155
171
|
# If this is not a Gemini model or no structured output is requested,
|
|
156
172
|
# use the parent implementation directly
|
|
157
173
|
if not self._is_gemini_model() or output_format is None:
|
|
158
174
|
return await super().ainvoke(messages, output_format)
|
|
159
|
-
|
|
160
|
-
# For Gemini models with structured output, we need to intercept and fix the schema
|
|
161
|
-
from browser_use.llm.openai.serializer import OpenAIMessageSerializer
|
|
162
|
-
from browser_use.llm.exceptions import ModelProviderError
|
|
163
|
-
from openai.types.shared_params.response_format_json_schema import JSONSchema, ResponseFormatJSONSchema
|
|
164
|
-
from typing import Any
|
|
165
|
-
from collections.abc import Iterable
|
|
166
|
-
from openai.types.chat import ChatCompletionContentPartTextParam
|
|
167
|
-
|
|
175
|
+
|
|
168
176
|
openai_messages = OpenAIMessageSerializer.serialize_messages(messages)
|
|
169
|
-
|
|
177
|
+
|
|
170
178
|
try:
|
|
171
179
|
model_params: dict[str, Any] = {}
|
|
172
|
-
|
|
180
|
+
|
|
173
181
|
if self.temperature is not None:
|
|
174
182
|
model_params['temperature'] = self.temperature
|
|
175
|
-
|
|
183
|
+
|
|
176
184
|
if self.frequency_penalty is not None:
|
|
177
185
|
model_params['frequency_penalty'] = self.frequency_penalty
|
|
178
|
-
|
|
186
|
+
|
|
179
187
|
if self.max_completion_tokens is not None:
|
|
180
188
|
model_params['max_completion_tokens'] = self.max_completion_tokens
|
|
181
|
-
|
|
189
|
+
|
|
182
190
|
if self.top_p is not None:
|
|
183
191
|
model_params['top_p'] = self.top_p
|
|
184
|
-
|
|
192
|
+
|
|
185
193
|
if self.seed is not None:
|
|
186
194
|
model_params['seed'] = self.seed
|
|
187
|
-
|
|
195
|
+
|
|
188
196
|
if self.service_tier is not None:
|
|
189
197
|
model_params['service_tier'] = self.service_tier
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
198
|
+
|
|
199
|
+
if self.reasoning_models and any(str(m).lower() in str(self.model).lower() for m in self.reasoning_models):
|
|
200
|
+
model_params['reasoning_effort'] = self.reasoning_effort
|
|
201
|
+
del model_params['temperature']
|
|
202
|
+
del model_params['frequency_penalty']
|
|
203
|
+
|
|
204
|
+
if output_format is None:
|
|
205
|
+
# Return string response
|
|
206
|
+
response = await self.get_client().chat.completions.create(
|
|
207
|
+
model=self.model,
|
|
208
|
+
messages=openai_messages,
|
|
209
|
+
**model_params,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
usage = self._get_usage(response)
|
|
213
|
+
return ChatInvokeCompletion(
|
|
214
|
+
completion=response.choices[0].message.content or '',
|
|
215
|
+
usage=usage,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
else:
|
|
219
|
+
original_schema = SchemaOptimizer.create_optimized_json_schema(output_format)
|
|
220
|
+
fixed_schema = self._fix_gemini_schema(original_schema)
|
|
221
|
+
response_format: JSONSchema = {
|
|
222
|
+
'name': 'agent_output',
|
|
223
|
+
'strict': True,
|
|
224
|
+
'schema': fixed_schema,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
# Add JSON schema to system prompt if requested
|
|
228
|
+
if self.add_schema_to_system_prompt and openai_messages and openai_messages[0]['role'] == 'system':
|
|
229
|
+
schema_text = f'\n<json_schema>\n{response_format}\n</json_schema>'
|
|
230
|
+
if isinstance(openai_messages[0]['content'], str):
|
|
231
|
+
openai_messages[0]['content'] += schema_text
|
|
232
|
+
elif isinstance(openai_messages[0]['content'], Iterable):
|
|
233
|
+
openai_messages[0]['content'] = list(openai_messages[0]['content']) + [
|
|
234
|
+
ChatCompletionContentPartTextParam(text=schema_text, type='text')
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
# Return structured response
|
|
238
|
+
response = await self.get_client().chat.completions.create(
|
|
239
|
+
model=self.model,
|
|
240
|
+
messages=openai_messages,
|
|
241
|
+
response_format=ResponseFormatJSONSchema(json_schema=response_format, type='json_schema'),
|
|
242
|
+
**model_params,
|
|
224
243
|
)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
244
|
+
|
|
245
|
+
if response.choices[0].message.content is None:
|
|
246
|
+
raise ModelProviderError(
|
|
247
|
+
message='Failed to parse structured output from model response',
|
|
248
|
+
status_code=500,
|
|
249
|
+
model=self.name,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
usage = self._get_usage(response)
|
|
253
|
+
|
|
254
|
+
parsed = output_format.model_validate_json(response.choices[0].message.content)
|
|
255
|
+
|
|
256
|
+
return ChatInvokeCompletion(
|
|
257
|
+
completion=parsed,
|
|
258
|
+
usage=usage,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
except RateLimitError as e:
|
|
262
|
+
error_message = e.response.json().get('error', {})
|
|
263
|
+
error_message = (
|
|
264
|
+
error_message.get('message', 'Unknown model error') if isinstance(error_message,
|
|
265
|
+
dict) else error_message
|
|
233
266
|
)
|
|
234
|
-
|
|
267
|
+
raise ModelProviderError(
|
|
268
|
+
message=error_message,
|
|
269
|
+
status_code=e.response.status_code,
|
|
270
|
+
model=self.name,
|
|
271
|
+
) from e
|
|
272
|
+
|
|
273
|
+
except APIConnectionError as e:
|
|
274
|
+
raise ModelProviderError(message=str(e), model=self.name) from e
|
|
275
|
+
|
|
276
|
+
except APIStatusError as e:
|
|
277
|
+
try:
|
|
278
|
+
error_message = e.response.json().get('error', {})
|
|
279
|
+
except Exception:
|
|
280
|
+
error_message = e.response.text
|
|
281
|
+
error_message = (
|
|
282
|
+
error_message.get('message', 'Unknown model error') if isinstance(error_message,
|
|
283
|
+
dict) else error_message
|
|
284
|
+
)
|
|
285
|
+
raise ModelProviderError(
|
|
286
|
+
message=error_message,
|
|
287
|
+
status_code=e.response.status_code,
|
|
288
|
+
model=self.name,
|
|
289
|
+
) from e
|
|
290
|
+
|
|
235
291
|
except Exception as e:
|
|
236
|
-
|
|
237
|
-
raise ModelProviderError(message=str(e), model=self.name) from e
|
|
292
|
+
raise ModelProviderError(message=str(e), model=self.name) from e
|
vibe_surf/logger.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Logger configuration for VibeSurf.
|
|
3
|
+
"""
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from logging.handlers import RotatingFileHandler
|
|
8
|
+
|
|
9
|
+
from .common import get_workspace_dir
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def setup_logger(name: str = "vibesurf") -> logging.Logger:
|
|
13
|
+
"""
|
|
14
|
+
Set up and configure the logger for VibeSurf.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
name (str): Logger name, defaults to "vibesurf"
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
logging.Logger: Configured logger instance
|
|
21
|
+
"""
|
|
22
|
+
# Get debug flag from environment variable
|
|
23
|
+
debug_mode = os.getenv("VIBESURF_DEBUG", "false").lower() in ("true", "1", "yes", "on")
|
|
24
|
+
log_level = logging.DEBUG if debug_mode else logging.INFO
|
|
25
|
+
|
|
26
|
+
# Create logger
|
|
27
|
+
logger = logging.getLogger(name)
|
|
28
|
+
logger.setLevel(log_level)
|
|
29
|
+
|
|
30
|
+
# Avoid adding handlers multiple times
|
|
31
|
+
if logger.handlers:
|
|
32
|
+
return logger
|
|
33
|
+
|
|
34
|
+
# Create formatter with file and line info
|
|
35
|
+
if log_level == logging.DEBUG:
|
|
36
|
+
formatter = logging.Formatter(
|
|
37
|
+
fmt='%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(funcName)s() - %(message)s',
|
|
38
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
39
|
+
)
|
|
40
|
+
else:
|
|
41
|
+
formatter = logging.Formatter(
|
|
42
|
+
fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
43
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Console handler - log to terminal
|
|
47
|
+
console_handler = logging.StreamHandler()
|
|
48
|
+
console_handler.setLevel(log_level)
|
|
49
|
+
console_handler.setFormatter(formatter)
|
|
50
|
+
logger.addHandler(console_handler)
|
|
51
|
+
|
|
52
|
+
# File handler - log to file
|
|
53
|
+
try:
|
|
54
|
+
workspace_dir = get_workspace_dir()
|
|
55
|
+
logs_dir = os.path.join(workspace_dir, "logs")
|
|
56
|
+
os.makedirs(logs_dir, exist_ok=True)
|
|
57
|
+
|
|
58
|
+
# Create log filename with current date
|
|
59
|
+
current_date = datetime.now().strftime("%Y-%m-%d")
|
|
60
|
+
log_filename = f"log_{current_date}.log"
|
|
61
|
+
log_filepath = os.path.join(logs_dir, log_filename)
|
|
62
|
+
|
|
63
|
+
# Use RotatingFileHandler to manage log file size
|
|
64
|
+
file_handler = RotatingFileHandler(
|
|
65
|
+
log_filepath,
|
|
66
|
+
maxBytes=10 * 1024 * 1024, # 10MB
|
|
67
|
+
backupCount=5,
|
|
68
|
+
encoding='utf-8'
|
|
69
|
+
)
|
|
70
|
+
file_handler.setLevel(log_level)
|
|
71
|
+
file_handler.setFormatter(formatter)
|
|
72
|
+
logger.addHandler(file_handler)
|
|
73
|
+
|
|
74
|
+
logger.info(f"Logger initialized. Log level: {logging.getLevelName(log_level)}")
|
|
75
|
+
logger.info(f"WorkSpace directory: {workspace_dir}")
|
|
76
|
+
logger.info(f"Log file: {log_filepath}")
|
|
77
|
+
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.error(f"Failed to setup file logging: {e}")
|
|
80
|
+
logger.warning("Continuing with console logging only")
|
|
81
|
+
|
|
82
|
+
return logger
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_logger(name: str = "vibesurf") -> logging.Logger:
|
|
86
|
+
"""
|
|
87
|
+
Get or create a logger instance.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
name (str): Logger name, defaults to "vibesurf"
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
logging.Logger: Logger instance
|
|
94
|
+
"""
|
|
95
|
+
return setup_logger(name)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# Create default logger instance
|
|
99
|
+
default_logger = get_logger()
|