semantio 0.0.3__py3-none-any.whl → 0.0.5__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.
- semantio/agent.py +97 -72
- semantio/memory.py +51 -8
- semantio/models.py +9 -0
- semantio/storage/__init__.py +5 -0
- semantio/storage/base_storage.py +12 -0
- semantio/storage/in_memory_storage.py +14 -0
- semantio/storage/local_storage.py +29 -0
- semantio/tools/web_browser.py +271 -0
- {semantio-0.0.3.dist-info → semantio-0.0.5.dist-info}/LICENSE +1 -1
- {semantio-0.0.3.dist-info → semantio-0.0.5.dist-info}/METADATA +5 -1
- {semantio-0.0.3.dist-info → semantio-0.0.5.dist-info}/RECORD +14 -10
- {semantio-0.0.3.dist-info → semantio-0.0.5.dist-info}/WHEEL +0 -0
- {semantio-0.0.3.dist-info → semantio-0.0.5.dist-info}/entry_points.txt +0 -0
- {semantio-0.0.3.dist-info → semantio-0.0.5.dist-info}/top_level.txt +0 -0
semantio/agent.py
CHANGED
@@ -16,6 +16,7 @@ from .tools.base_tool import BaseTool
|
|
16
16
|
from pathlib import Path
|
17
17
|
import importlib
|
18
18
|
import os
|
19
|
+
from .memory import Memory
|
19
20
|
|
20
21
|
# Configure logging
|
21
22
|
logging.basicConfig(level=logging.INFO)
|
@@ -48,6 +49,13 @@ class Agent(BaseModel):
|
|
48
49
|
semantic_model: Optional[Any] = Field(None, description="SentenceTransformer model for semantic matching.")
|
49
50
|
team: Optional[List['Agent']] = Field(None, description="List of assistants in the team.")
|
50
51
|
auto_tool: bool = Field(False, description="Whether to automatically detect and call tools.")
|
52
|
+
memory: Memory = Field(default_factory=Memory)
|
53
|
+
memory_config: Dict = Field(
|
54
|
+
default_factory=lambda: {
|
55
|
+
"max_context_length": 4000,
|
56
|
+
"summarization_threshold": 3000
|
57
|
+
}
|
58
|
+
)
|
51
59
|
|
52
60
|
# Allow arbitrary types
|
53
61
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
@@ -56,8 +64,16 @@ class Agent(BaseModel):
|
|
56
64
|
super().__init__(**kwargs)
|
57
65
|
# Initialize the model and tools here if needed
|
58
66
|
self._initialize_model()
|
59
|
-
#
|
67
|
+
# Initialize memory with config
|
68
|
+
self.memory = Memory(
|
69
|
+
max_context_length=self.memory_config.get("max_context_length", 4000),
|
70
|
+
summarization_threshold=self.memory_config.get("summarization_threshold", 3000)
|
71
|
+
)
|
72
|
+
# Initialize tools as an empty list if not provided
|
60
73
|
if self.tools is None:
|
74
|
+
self.tools = []
|
75
|
+
# Automatically discover and register tools if auto tool is enabled
|
76
|
+
if self.auto_tool and not self.tools:
|
61
77
|
self.tools = self._discover_tools()
|
62
78
|
# Pass the LLM instance to each tool
|
63
79
|
for tool in self.tools:
|
@@ -213,23 +229,33 @@ class Agent(BaseModel):
|
|
213
229
|
message: Optional[Union[str, Image, List, Dict]] = None,
|
214
230
|
stream: bool = False,
|
215
231
|
markdown: bool = False,
|
216
|
-
tools: Optional[List[BaseTool]] = None,
|
217
232
|
team: Optional[List['Agent']] = None,
|
218
233
|
**kwargs,
|
219
|
-
) -> Union[str, Dict]:
|
234
|
+
) -> Union[str, Dict]:
|
220
235
|
"""Print the agent's response to the console and return it."""
|
236
|
+
|
237
|
+
# Store user message if provided
|
238
|
+
if message and isinstance(message, str):
|
239
|
+
self.memory.add_message(role="user", content=message)
|
221
240
|
|
222
241
|
if stream:
|
223
242
|
# Handle streaming response
|
224
243
|
response = ""
|
225
244
|
for chunk in self._stream_response(message, markdown=markdown, **kwargs):
|
226
|
-
print(chunk)
|
245
|
+
print(chunk, end="", flush=True)
|
227
246
|
response += chunk
|
247
|
+
# Store agent response
|
248
|
+
if response:
|
249
|
+
self.memory.add_message(role="assistant", content=response)
|
250
|
+
print() # New line after streaming
|
228
251
|
return response
|
229
252
|
else:
|
230
253
|
# Generate and return the response
|
231
|
-
response = self._generate_response(message, markdown=markdown,
|
254
|
+
response = self._generate_response(message, markdown=markdown, team=team, **kwargs)
|
232
255
|
print(response) # Print the response to the console
|
256
|
+
# Store agent response
|
257
|
+
if response:
|
258
|
+
self.memory.add_message(role="assistant", content=response)
|
233
259
|
return response
|
234
260
|
|
235
261
|
|
@@ -245,43 +271,6 @@ class Agent(BaseModel):
|
|
245
271
|
if self.tools is None:
|
246
272
|
self.tools = []
|
247
273
|
self.tools.append(tool)
|
248
|
-
|
249
|
-
def _detect_tool_call(self, message: str) -> Optional[Dict[str, Any]]:
|
250
|
-
"""
|
251
|
-
Use the LLM to detect which tool should be called based on the user's query.
|
252
|
-
"""
|
253
|
-
if not self.tools:
|
254
|
-
logger.warning("No tools available to detect.")
|
255
|
-
return None
|
256
|
-
|
257
|
-
# Create a prompt for the LLM
|
258
|
-
prompt = f"""
|
259
|
-
You are an AI agent that helps users by selecting the most appropriate tool to answer their query. Below is a list of available tools and their functionalities:
|
260
|
-
|
261
|
-
{self._get_tool_descriptions()}
|
262
|
-
|
263
|
-
Based on the user's query, select the most appropriate tool. Respond with the name of the tool (e.g., "CryptoPriceChecker"). If no tool is suitable, respond with "None".
|
264
|
-
|
265
|
-
User Query: "{message}"
|
266
|
-
"""
|
267
|
-
|
268
|
-
try:
|
269
|
-
# Call the LLM to generate the response
|
270
|
-
response = self.llm_instance.generate(prompt=prompt)
|
271
|
-
tool_name = response.strip().replace('"', '').replace("'", "")
|
272
|
-
|
273
|
-
# Find the tool in the list of available tools
|
274
|
-
tool = next((t for t in self.tools if t.name.lower() == tool_name.lower()), None)
|
275
|
-
if tool:
|
276
|
-
logger.info(f"Detected tool call: {tool.name}")
|
277
|
-
return {
|
278
|
-
"tool": tool.name,
|
279
|
-
"input": {"query": message}
|
280
|
-
}
|
281
|
-
except Exception as e:
|
282
|
-
logger.error(f"Failed to detect tool call: {e}")
|
283
|
-
|
284
|
-
return None
|
285
274
|
|
286
275
|
def _analyze_query_and_select_tools(self, query: str) -> List[Dict[str, Any]]:
|
287
276
|
"""
|
@@ -324,18 +313,15 @@ class Agent(BaseModel):
|
|
324
313
|
return []
|
325
314
|
|
326
315
|
|
327
|
-
def _generate_response(self, message: str, markdown: bool = False,
|
316
|
+
def _generate_response(self, message: str, markdown: bool = False, team: Optional[List['Agent']] = None, **kwargs) -> str:
|
328
317
|
"""Generate the agent's response, including tool execution and context retrieval."""
|
329
|
-
# Use the specified
|
330
|
-
if tools is not None:
|
331
|
-
self.tools = tools
|
318
|
+
# Use the specified team if provided
|
332
319
|
if team is not None:
|
333
320
|
return self._generate_team_response(message, team, markdown=markdown, **kwargs)
|
334
|
-
|
335
321
|
# Initialize tool_outputs as an empty dictionary
|
336
322
|
tool_outputs = {}
|
337
323
|
responses = []
|
338
|
-
|
324
|
+
tool_calls = []
|
339
325
|
# Use the LLM to analyze the query and dynamically select tools when auto_tool is enabled
|
340
326
|
if self.auto_tool:
|
341
327
|
tool_calls = self._analyze_query_and_select_tools(message)
|
@@ -344,7 +330,7 @@ class Agent(BaseModel):
|
|
344
330
|
if self.tools:
|
345
331
|
tool_calls = [
|
346
332
|
{
|
347
|
-
"tool": tool.
|
333
|
+
"tool": tool.name,
|
348
334
|
"input": {
|
349
335
|
"query": message, # Use the message as the query
|
350
336
|
"context": None, # No context provided by default
|
@@ -352,10 +338,8 @@ class Agent(BaseModel):
|
|
352
338
|
}
|
353
339
|
for tool in self.tools
|
354
340
|
]
|
355
|
-
else:
|
356
|
-
tool_calls = kwargs.get("tool_calls", [])
|
357
341
|
|
358
|
-
|
342
|
+
# Execute tools if any are detected
|
359
343
|
if tool_calls:
|
360
344
|
for tool_call in tool_calls:
|
361
345
|
tool_name = tool_call["tool"]
|
@@ -385,41 +369,50 @@ class Agent(BaseModel):
|
|
385
369
|
try:
|
386
370
|
# Prepare the context for the LLM
|
387
371
|
context = {
|
372
|
+
"conversation_history": self.memory.get_context(self.llm_instance),
|
388
373
|
"tool_outputs": tool_outputs,
|
389
374
|
"rag_context": self.rag.retrieve(message) if self.rag else None,
|
390
|
-
"
|
375
|
+
"knowledge_base": self._get_knowledge_context(message) if self.knowledge_base else None,
|
391
376
|
}
|
392
|
-
|
377
|
+
# 3. Build a memory-aware prompt.
|
378
|
+
prompt = self._build_memory_prompt(message, context)
|
379
|
+
# To (convert MemoryEntry objects to dicts and remove metadata):
|
380
|
+
memory_entries = [{"role": e.role, "content": e.content} for e in self.memory.storage.retrieve()]
|
393
381
|
# Generate a response using the LLM
|
394
|
-
llm_response = self.llm_instance.generate(prompt=
|
382
|
+
llm_response = self.llm_instance.generate(prompt=prompt, context=context, memory=memory_entries, **kwargs)
|
395
383
|
responses.append(f"**Analysis:**\n\n{llm_response}")
|
396
384
|
except Exception as e:
|
397
385
|
logger.error(f"Failed to generate LLM response: {e}")
|
398
386
|
responses.append(f"An error occurred while generating the analysis: {e}")
|
399
|
-
if not tool_calls:
|
387
|
+
if not self.tools and not tool_calls:
|
400
388
|
# If no tools were executed, proceed with the original logic
|
401
389
|
# Retrieve relevant context using RAG
|
402
390
|
rag_context = self.rag.retrieve(message) if self.rag else None
|
403
391
|
# Retrieve relevant context from the knowledge base (API result)
|
404
|
-
knowledge_base_context = None
|
405
|
-
if self.knowledge_base:
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
392
|
+
# knowledge_base_context = None
|
393
|
+
# if self.knowledge_base:
|
394
|
+
# # Flatten the knowledge base
|
395
|
+
# flattened_data = self._flatten_data(self.knowledge_base)
|
396
|
+
# # Find all relevant key-value pairs in the knowledge base
|
397
|
+
# relevant_values = self._find_all_relevant_keys(message, flattened_data)
|
398
|
+
# if relevant_values:
|
399
|
+
# knowledge_base_context = ", ".join(relevant_values)
|
412
400
|
|
413
401
|
# Combine both contexts (RAG and knowledge base)
|
414
402
|
context = {
|
403
|
+
"conversation_history": self.memory.get_context(self.llm_instance),
|
415
404
|
"rag_context": rag_context,
|
416
|
-
"
|
405
|
+
"knowledge_base": self._get_knowledge_context(message),
|
417
406
|
}
|
418
407
|
# Prepare the prompt with instructions, description, and context
|
419
|
-
|
408
|
+
# 3. Build a memory-aware prompt.
|
409
|
+
prompt = self._build_memory_prompt(message, context)
|
410
|
+
# To (convert MemoryEntry objects to dicts and remove metadata):
|
411
|
+
memory_entries = [{"role": e.role, "content": e.content} for e in self.memory.storage.retrieve()]
|
420
412
|
|
421
413
|
# Generate the response using the LLM
|
422
|
-
response = self.llm_instance.generate(prompt=prompt, context=context, **kwargs)
|
414
|
+
response = self.llm_instance.generate(prompt=prompt, context=context, memory=memory_entries, **kwargs)
|
415
|
+
|
423
416
|
|
424
417
|
# Format the response based on the json_output flag
|
425
418
|
if self.json_output:
|
@@ -432,9 +425,37 @@ class Agent(BaseModel):
|
|
432
425
|
if markdown:
|
433
426
|
return f"**Response:**\n\n{response}"
|
434
427
|
return response
|
435
|
-
# Combine all responses into a single string
|
436
428
|
return "\n\n".join(responses)
|
437
429
|
|
430
|
+
# Modified prompt construction with memory integration
|
431
|
+
def _build_memory_prompt(self, user_input: str, context: dict) -> str:
|
432
|
+
"""Enhanced prompt builder with memory context."""
|
433
|
+
prompt_parts = []
|
434
|
+
|
435
|
+
if self.description:
|
436
|
+
prompt_parts.append(f"# ROLE\n{self.description}")
|
437
|
+
|
438
|
+
if self.instructions:
|
439
|
+
prompt_parts.append(f"# INSTRUCTIONS\n" + "\n".join(f"- {i}" for i in self.instructions))
|
440
|
+
|
441
|
+
if context['conversation_history']:
|
442
|
+
prompt_parts.append(f"# CONVERSATION HISTORY\n{context['conversation_history']}")
|
443
|
+
|
444
|
+
if context['knowledge_base']:
|
445
|
+
prompt_parts.append(f"# KNOWLEDGE BASE\n{context['knowledge_base']}")
|
446
|
+
|
447
|
+
prompt_parts.append(f"# USER INPUT\n{user_input}")
|
448
|
+
|
449
|
+
return "\n\n".join(prompt_parts)
|
450
|
+
|
451
|
+
def _get_knowledge_context(self, message: str) -> str:
|
452
|
+
"""Retrieve and format knowledge base context."""
|
453
|
+
if not self.knowledge_base:
|
454
|
+
return ""
|
455
|
+
|
456
|
+
flattened = self._flatten_data(self.knowledge_base)
|
457
|
+
relevant = self._find_all_relevant_keys(message, flattened)
|
458
|
+
return "\n".join(f"- {item}" for item in relevant) if relevant else ""
|
438
459
|
def _generate_team_response(self, message: str, team: List['Agent'], markdown: bool = False, **kwargs) -> str:
|
439
460
|
"""Generate a response using a team of assistants."""
|
440
461
|
responses = []
|
@@ -581,17 +602,21 @@ class Agent(BaseModel):
|
|
581
602
|
"""Run the agent in a CLI app."""
|
582
603
|
from rich.prompt import Prompt
|
583
604
|
|
605
|
+
# Print initial message if provided
|
584
606
|
if message:
|
585
607
|
self.print_response(message=message, **kwargs)
|
586
608
|
|
587
609
|
_exit_on = exit_on or ["exit", "quit", "bye"]
|
588
610
|
while True:
|
589
|
-
|
590
|
-
|
611
|
+
try:
|
612
|
+
message = Prompt.ask(f"[bold] {self.emoji} {self.user_name} [/bold]")
|
613
|
+
if message in _exit_on:
|
614
|
+
break
|
615
|
+
self.print_response(message=message, **kwargs)
|
616
|
+
except KeyboardInterrupt:
|
617
|
+
print("\n\nSession ended. Goodbye!")
|
591
618
|
break
|
592
619
|
|
593
|
-
self.print_response(message=message, **kwargs)
|
594
|
-
|
595
620
|
def _generate_api(self):
|
596
621
|
"""Generate an API for the agent if api=True."""
|
597
622
|
from .api.api_generator import APIGenerator
|
semantio/memory.py
CHANGED
@@ -1,11 +1,54 @@
|
|
1
|
-
from
|
2
|
-
|
1
|
+
from .models import MemoryEntry
|
2
|
+
from .storage import BaseMemoryStorage, InMemoryStorage, FileStorage
|
3
|
+
from typing import List, Dict, Optional
|
4
|
+
from .llm.base_llm import BaseLLM
|
3
5
|
class Memory:
|
4
|
-
def __init__(
|
5
|
-
self
|
6
|
+
def __init__(
|
7
|
+
self,
|
8
|
+
storage: BaseMemoryStorage = InMemoryStorage(),
|
9
|
+
max_context_length: int = 4000,
|
10
|
+
summarization_threshold: int = 3000
|
11
|
+
):
|
12
|
+
self.storage = storage
|
13
|
+
self.max_context_length = max_context_length
|
14
|
+
self.summarization_threshold = summarization_threshold
|
15
|
+
self._current_context = ""
|
16
|
+
|
17
|
+
def add_message(self, role: str, content: str, metadata: Optional[Dict] = None):
|
18
|
+
entry = MemoryEntry(
|
19
|
+
role=role,
|
20
|
+
content=content,
|
21
|
+
metadata=metadata or {}
|
22
|
+
)
|
23
|
+
self.storage.store(entry)
|
24
|
+
self._manage_context()
|
25
|
+
|
26
|
+
def get_context(self, llm: Optional[BaseLLM] = None) -> str:
|
27
|
+
if len(self._current_context) < self.summarization_threshold:
|
28
|
+
return self._current_context
|
29
|
+
|
30
|
+
# Automatic summarization when context grows too large
|
31
|
+
if llm:
|
32
|
+
return self.summarize(llm)
|
33
|
+
return self._current_context[:self.max_context_length]
|
34
|
+
def _manage_context(self):
|
35
|
+
# Include roles in the conversation history
|
36
|
+
full_history = "\n".join([f"{e.role}: {e.content}" for e in self.storage.retrieve()])
|
37
|
+
if len(full_history) > self.max_context_length:
|
38
|
+
self._current_context = full_history[-self.max_context_length:]
|
39
|
+
else:
|
40
|
+
self._current_context = full_history
|
6
41
|
|
7
|
-
def
|
8
|
-
|
42
|
+
def summarize(self, llm: BaseLLM) -> str:
|
43
|
+
# Include roles in the history for summarization
|
44
|
+
history = "\n".join([f"{e.role}: {e.content}" for e in self.storage.retrieve()])
|
45
|
+
prompt = f"""
|
46
|
+
Summarize this conversation history maintaining key details and references:
|
47
|
+
{history[-self.summarization_threshold:]}
|
48
|
+
"""
|
49
|
+
self._current_context = llm.generate(prompt)
|
50
|
+
return self._current_context
|
9
51
|
|
10
|
-
def
|
11
|
-
|
52
|
+
def clear(self):
|
53
|
+
self.storage = InMemoryStorage()
|
54
|
+
self._current_context = ""
|
semantio/models.py
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
from pydantic import BaseModel, Field
|
2
|
+
from datetime import datetime
|
3
|
+
from typing import Dict
|
4
|
+
|
5
|
+
class MemoryEntry(BaseModel):
|
6
|
+
role: str # "user" or "assistant"
|
7
|
+
content: str
|
8
|
+
timestamp: datetime = Field(default_factory=datetime.now)
|
9
|
+
metadata: Dict = Field(default_factory=dict)
|
semantio/storage/__init__.py
CHANGED
@@ -0,0 +1,12 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from typing import List, Optional
|
3
|
+
from ..models import MemoryEntry
|
4
|
+
|
5
|
+
class BaseMemoryStorage(ABC):
|
6
|
+
@abstractmethod
|
7
|
+
def store(self, entry: MemoryEntry):
|
8
|
+
pass
|
9
|
+
|
10
|
+
@abstractmethod
|
11
|
+
def retrieve(self, query: Optional[str] = None, limit: int = 20) -> List[MemoryEntry]:
|
12
|
+
pass
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# hashai/storage/in_memory_storage.py
|
2
|
+
from typing import List, Optional
|
3
|
+
from ..models import MemoryEntry
|
4
|
+
from .base_storage import BaseMemoryStorage
|
5
|
+
|
6
|
+
class InMemoryStorage(BaseMemoryStorage):
|
7
|
+
def __init__(self):
|
8
|
+
self.history: List[MemoryEntry] = []
|
9
|
+
|
10
|
+
def store(self, entry: MemoryEntry):
|
11
|
+
self.history.append(entry)
|
12
|
+
|
13
|
+
def retrieve(self, query: Optional[str] = None, limit: int = 10) -> List[MemoryEntry]:
|
14
|
+
return self.history[-limit:]
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import json
|
2
|
+
from typing import List, Optional
|
3
|
+
from ..models import MemoryEntry
|
4
|
+
from .base_storage import BaseMemoryStorage
|
5
|
+
|
6
|
+
class FileStorage(BaseMemoryStorage):
|
7
|
+
def __init__(self, file_path: str = "memory.json"):
|
8
|
+
self.file_path = file_path
|
9
|
+
self.history = self._load_from_file()
|
10
|
+
|
11
|
+
def _load_from_file(self) -> List[MemoryEntry]:
|
12
|
+
try:
|
13
|
+
with open(self.file_path, "r") as f:
|
14
|
+
data = json.load(f)
|
15
|
+
return [MemoryEntry(**entry) for entry in data]
|
16
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
17
|
+
return []
|
18
|
+
|
19
|
+
def _save_to_file(self):
|
20
|
+
with open(self.file_path, "w") as f:
|
21
|
+
data = [entry.dict() for entry in self.history]
|
22
|
+
json.dump(data, f, default=str)
|
23
|
+
|
24
|
+
def store(self, entry: MemoryEntry):
|
25
|
+
self.history.append(entry)
|
26
|
+
self._save_to_file()
|
27
|
+
|
28
|
+
def retrieve(self, query: Optional[str] = None, limit: int = 20) -> List[MemoryEntry]:
|
29
|
+
return self.history[-limit:]
|
@@ -0,0 +1,271 @@
|
|
1
|
+
# web_browser.py
|
2
|
+
from typing import Dict, Any, List, Optional
|
3
|
+
from pydantic import Field, BaseModel
|
4
|
+
from selenium import webdriver
|
5
|
+
from selenium.webdriver.common.by import By
|
6
|
+
from selenium.webdriver.support.ui import WebDriverWait
|
7
|
+
from selenium.webdriver.support import expected_conditions as EC
|
8
|
+
from selenium.webdriver.chrome.options import Options
|
9
|
+
from selenium.webdriver.chrome.service import Service
|
10
|
+
from webdriver_manager.chrome import ChromeDriverManager
|
11
|
+
from bs4 import BeautifulSoup
|
12
|
+
import json
|
13
|
+
import time
|
14
|
+
import re
|
15
|
+
import logging
|
16
|
+
from .base_tool import BaseTool
|
17
|
+
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
class BrowserPlan(BaseModel):
|
21
|
+
tasks: List[Dict[str, Any]] = Field(
|
22
|
+
...,
|
23
|
+
description="List of automation tasks to execute"
|
24
|
+
)
|
25
|
+
|
26
|
+
class WebBrowserTool(BaseTool):
|
27
|
+
name: str = Field("WebBrowser", description="Name of the tool")
|
28
|
+
description: str = Field(
|
29
|
+
"Universal web automation tool for dynamic website interactions",
|
30
|
+
description="Tool description"
|
31
|
+
)
|
32
|
+
|
33
|
+
def execute(self, input: Dict[str, Any]) -> Dict[str, Any]:
|
34
|
+
"""Execute dynamic web automation workflow"""
|
35
|
+
driver = None
|
36
|
+
try:
|
37
|
+
driver = self._init_browser(input.get("headless", False))
|
38
|
+
results = []
|
39
|
+
current_url = ""
|
40
|
+
|
41
|
+
# Generate initial plan
|
42
|
+
plan = self._generate_plan(input['query'], current_url)
|
43
|
+
|
44
|
+
for task in plan.tasks:
|
45
|
+
result = self._execute_safe_task(driver, task)
|
46
|
+
results.append(result)
|
47
|
+
|
48
|
+
if not result['success']:
|
49
|
+
break
|
50
|
+
|
51
|
+
# Update context for next tasks
|
52
|
+
current_url = driver.current_url
|
53
|
+
|
54
|
+
return {"status": "success", "results": results}
|
55
|
+
|
56
|
+
except Exception as e:
|
57
|
+
return {"status": "error", "message": str(e)}
|
58
|
+
finally:
|
59
|
+
if driver:
|
60
|
+
driver.quit()
|
61
|
+
|
62
|
+
def _init_browser(self, headless: bool) -> webdriver.Chrome:
|
63
|
+
"""Initialize browser with advanced options"""
|
64
|
+
options = Options()
|
65
|
+
options.add_argument("--start-maximized")
|
66
|
+
options.add_argument("--disable-blink-features=AutomationControlled")
|
67
|
+
options.add_experimental_option("excludeSwitches", ["enable-automation"])
|
68
|
+
|
69
|
+
if headless:
|
70
|
+
options.add_argument("--headless=new")
|
71
|
+
|
72
|
+
return webdriver.Chrome(
|
73
|
+
service=Service(ChromeDriverManager().install()),
|
74
|
+
options=options
|
75
|
+
)
|
76
|
+
|
77
|
+
def _generate_plan(self, query: str, current_url: str) -> BrowserPlan:
|
78
|
+
"""Generate adaptive execution plan using LLM"""
|
79
|
+
prompt = f"""Generate browser automation plan for: {query}
|
80
|
+
|
81
|
+
Current URL: {current_url or 'No page loaded yet'}
|
82
|
+
|
83
|
+
Required JSON format:
|
84
|
+
{{
|
85
|
+
"tasks": [
|
86
|
+
{{
|
87
|
+
"action": "navigate|click|type|wait|scroll",
|
88
|
+
"selector": "CSS selector (optional)",
|
89
|
+
"value": "input text/URL/seconds",
|
90
|
+
"description": "action purpose"
|
91
|
+
}}
|
92
|
+
]
|
93
|
+
}}
|
94
|
+
|
95
|
+
Guidelines:
|
96
|
+
1. Prefer IDs in selectors (#element-id)
|
97
|
+
2. Use semantic attributes (aria-label, name)
|
98
|
+
3. Include wait steps after navigation
|
99
|
+
4. Prioritize visible elements
|
100
|
+
5. Add scroll steps for hidden elements
|
101
|
+
"""
|
102
|
+
|
103
|
+
response = self.llm.generate(prompt=prompt)
|
104
|
+
return self._parse_plan(response)
|
105
|
+
|
106
|
+
def _parse_plan(self, response: str) -> BrowserPlan:
|
107
|
+
"""Robust JSON parsing with multiple fallback strategies"""
|
108
|
+
try:
|
109
|
+
# Try extracting JSON from markdown code block
|
110
|
+
json_match = re.search(r'```json\n?(.+?)\n?```', response, re.DOTALL)
|
111
|
+
if json_match:
|
112
|
+
plan_data = json.loads(json_match.group(1).strip())
|
113
|
+
else:
|
114
|
+
# Fallback to extract first JSON object
|
115
|
+
json_str = re.search(r'\{.*\}', response, re.DOTALL).group()
|
116
|
+
plan_data = json.loads(json_str)
|
117
|
+
|
118
|
+
# Validate tasks structure
|
119
|
+
validated_tasks = []
|
120
|
+
for task in plan_data.get("tasks", []):
|
121
|
+
if not all(key in task for key in ["action", "description"]):
|
122
|
+
continue
|
123
|
+
validated_tasks.append({
|
124
|
+
"action": task["action"],
|
125
|
+
"selector": task.get("selector", ""),
|
126
|
+
"value": task.get("value", ""),
|
127
|
+
"description": task["description"]
|
128
|
+
})
|
129
|
+
|
130
|
+
return BrowserPlan(tasks=validated_tasks)
|
131
|
+
|
132
|
+
except (json.JSONDecodeError, AttributeError) as e:
|
133
|
+
logger.error(f"Plan parsing failed: {e}")
|
134
|
+
return BrowserPlan(tasks=[])
|
135
|
+
|
136
|
+
def _execute_safe_task(self, driver, task: Dict) -> Dict[str, Any]:
|
137
|
+
"""Execute task with comprehensive error handling"""
|
138
|
+
try:
|
139
|
+
action = task["action"].lower()
|
140
|
+
selector = task.get("selector", "")
|
141
|
+
value = task.get("value", "")
|
142
|
+
|
143
|
+
if action == "navigate":
|
144
|
+
return self._handle_navigation(driver, value)
|
145
|
+
|
146
|
+
elif action == "click":
|
147
|
+
return self._handle_click(driver, selector)
|
148
|
+
|
149
|
+
elif action == "type":
|
150
|
+
return self._handle_typing(driver, selector, value)
|
151
|
+
|
152
|
+
elif action == "wait":
|
153
|
+
return self._handle_wait(value)
|
154
|
+
|
155
|
+
elif action == "scroll":
|
156
|
+
return self._handle_scroll(driver, selector)
|
157
|
+
|
158
|
+
return {
|
159
|
+
"action": action,
|
160
|
+
"success": False,
|
161
|
+
"message": f"Unsupported action: {action}"
|
162
|
+
}
|
163
|
+
|
164
|
+
except Exception as e:
|
165
|
+
return {
|
166
|
+
"action": action,
|
167
|
+
"success": False,
|
168
|
+
"message": f"Critical error: {str(e)}"
|
169
|
+
}
|
170
|
+
|
171
|
+
def _handle_navigation(self, driver, url: str) -> Dict[str, Any]:
|
172
|
+
"""Smart navigation handler"""
|
173
|
+
if not url.startswith(("http://", "https://")):
|
174
|
+
url = f"https://{url}"
|
175
|
+
|
176
|
+
try:
|
177
|
+
driver.get(url)
|
178
|
+
WebDriverWait(driver, 15).until(
|
179
|
+
EC.presence_of_element_located((By.TAG_NAME, "body"))
|
180
|
+
)
|
181
|
+
return {
|
182
|
+
"action": "navigate",
|
183
|
+
"success": True,
|
184
|
+
"message": f"Navigated to {url}"
|
185
|
+
}
|
186
|
+
except Exception as e:
|
187
|
+
return {
|
188
|
+
"action": "navigate",
|
189
|
+
"success": False,
|
190
|
+
"message": f"Navigation failed: {str(e)}"
|
191
|
+
}
|
192
|
+
|
193
|
+
def _handle_click(self, driver, selector: str) -> Dict[str, Any]:
|
194
|
+
"""Dynamic click handler"""
|
195
|
+
try:
|
196
|
+
element = WebDriverWait(driver, 15).until(
|
197
|
+
EC.element_to_be_clickable((By.CSS_SELECTOR, selector))
|
198
|
+
)
|
199
|
+
driver.execute_script("arguments[0].scrollIntoView({behavior: 'smooth'});", element)
|
200
|
+
element.click()
|
201
|
+
return {
|
202
|
+
"action": "click",
|
203
|
+
"success": True,
|
204
|
+
"message": f"Clicked element: {selector}"
|
205
|
+
}
|
206
|
+
except Exception as e:
|
207
|
+
return {
|
208
|
+
"action": "click",
|
209
|
+
"success": False,
|
210
|
+
"message": f"Click failed: {str(e)}"
|
211
|
+
}
|
212
|
+
|
213
|
+
def _handle_typing(self, driver, selector: str, text: str) -> Dict[str, Any]:
|
214
|
+
"""Universal typing handler"""
|
215
|
+
try:
|
216
|
+
element = WebDriverWait(driver, 15).until(
|
217
|
+
EC.presence_of_element_located((By.CSS_SELECTOR, selector))
|
218
|
+
)
|
219
|
+
element.clear()
|
220
|
+
element.send_keys(text)
|
221
|
+
return {
|
222
|
+
"action": "type",
|
223
|
+
"success": True,
|
224
|
+
"message": f"Typed '{text}' into {selector}"
|
225
|
+
}
|
226
|
+
except Exception as e:
|
227
|
+
return {
|
228
|
+
"action": "type",
|
229
|
+
"success": False,
|
230
|
+
"message": f"Typing failed: {str(e)}"
|
231
|
+
}
|
232
|
+
|
233
|
+
def _handle_wait(self, seconds: str) -> Dict[str, Any]:
|
234
|
+
"""Configurable wait handler"""
|
235
|
+
try:
|
236
|
+
wait_time = float(seconds)
|
237
|
+
time.sleep(wait_time)
|
238
|
+
return {
|
239
|
+
"action": "wait",
|
240
|
+
"success": True,
|
241
|
+
"message": f"Waited {wait_time} seconds"
|
242
|
+
}
|
243
|
+
except ValueError:
|
244
|
+
return {
|
245
|
+
"action": "wait",
|
246
|
+
"success": False,
|
247
|
+
"message": "Invalid wait time"
|
248
|
+
}
|
249
|
+
|
250
|
+
def _handle_scroll(self, driver, selector: str) -> Dict[str, Any]:
|
251
|
+
"""Smart scroll handler"""
|
252
|
+
try:
|
253
|
+
if selector:
|
254
|
+
element = WebDriverWait(driver, 15).until(
|
255
|
+
EC.presence_of_element_located((By.CSS_SELECTOR, selector))
|
256
|
+
)
|
257
|
+
driver.execute_script("arguments[0].scrollIntoView({behavior: 'smooth'});", element)
|
258
|
+
else:
|
259
|
+
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
|
260
|
+
|
261
|
+
return {
|
262
|
+
"action": "scroll",
|
263
|
+
"success": True,
|
264
|
+
"message": f"Scrolled to {selector or 'page bottom'}"
|
265
|
+
}
|
266
|
+
except Exception as e:
|
267
|
+
return {
|
268
|
+
"action": "scroll",
|
269
|
+
"success": False,
|
270
|
+
"message": f"Scroll failed: {str(e)}"
|
271
|
+
}
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: semantio
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.5
|
4
4
|
Summary: A powerful SDK for building AI agents
|
5
5
|
Home-page: https://github.com/Syenah/semantio
|
6
6
|
Author: Rakesh
|
@@ -33,6 +33,10 @@ Requires-Dist: sentence-transformers
|
|
33
33
|
Requires-Dist: fuzzywuzzy
|
34
34
|
Requires-Dist: duckduckgo-search
|
35
35
|
Requires-Dist: yfinance
|
36
|
+
Requires-Dist: selenium
|
37
|
+
Requires-Dist: beautifulsoup4
|
38
|
+
Requires-Dist: webdriver-manager
|
39
|
+
Requires-Dist: validators
|
36
40
|
|
37
41
|
# Semantio: The Mother of Your AI Agents
|
38
42
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
semantio/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
semantio/agent.py,sha256=
|
3
|
-
semantio/memory.py,sha256=
|
2
|
+
semantio/agent.py,sha256=uPFz1WP2eb-z-tryQOX8necS8_tv4Il6qxNmZux9hNk,31709
|
3
|
+
semantio/memory.py,sha256=en9n3UySnj4rA0x3uR1sEdEzA7EkboQNbEHQ5KuEehw,2115
|
4
|
+
semantio/models.py,sha256=7hmP-F_aSU8WvsG3NGeC_hep-rUbiSbjUFMDVbpKxQE,289
|
4
5
|
semantio/rag.py,sha256=ROy3Pa1NURcDs6qQZ8IMoa5Xlzt6I-msEq0C1p8UgB0,472
|
5
6
|
semantio/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
7
|
semantio/api/api_generator.py,sha256=Q-USITEpluRESEaQuOmF7m1vhLKYU9P8eGlQppKT9J4,829
|
@@ -19,23 +20,26 @@ semantio/llm/gemini.py,sha256=er3zv1jOvWQBGbPuv4fS4pR_c_abHyhroe-rkXupOO4,1959
|
|
19
20
|
semantio/llm/groq.py,sha256=1AH30paKzDIQjBjWPQPN44QwFHsIOVwI-a587-cDIVc,4285
|
20
21
|
semantio/llm/mistral.py,sha256=NpvaB1cE6-jMEBdT0mTf6Ca4Qq2LS8QivDKI6AgdRjE,1061
|
21
22
|
semantio/llm/openai.py,sha256=I3ab-d_zFxm-TDhYk6t1PzDtElPJEEQ2eSiARBNIGi4,5174
|
22
|
-
semantio/storage/__init__.py,sha256=
|
23
|
+
semantio/storage/__init__.py,sha256=bGSJjA1qk6DUDrBijmWcQk3Y1a2K00MPoKI5KH43Ang,196
|
24
|
+
semantio/storage/base_storage.py,sha256=R9tQfidVZlCN6CyvnhB-Tc2lIZ7yQsyX4cbMoud64XM,336
|
23
25
|
semantio/storage/cloud_storage.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
24
|
-
semantio/storage/
|
26
|
+
semantio/storage/in_memory_storage.py,sha256=aZT8rRHF6Kz_udaqf0rux7XRFKf9Hr3d4c3Ylry7J14,474
|
27
|
+
semantio/storage/local_storage.py,sha256=Z8jCPo2MwZ8tuhQywWkHyxTrdSyYtzAPSNd46DTCth8,1007
|
25
28
|
semantio/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
26
29
|
semantio/tools/base_tool.py,sha256=xBNSa_8a8WmA4BGRLG2dE7wj9GnBcZo7-P2SyD86GvY,571
|
27
30
|
semantio/tools/crypto.py,sha256=mut1ztvpPcUUP3b563dh_FmKtP68KmNis3Qm8WENj8w,5559
|
28
31
|
semantio/tools/duckduckgo.py,sha256=6mGn0js0cIsVxQlAgB8AYNLP05H8WmJKnSVosiO9iH0,5034
|
29
32
|
semantio/tools/stocks.py,sha256=BVuK61O9OmWQjj0YdiCJY6TzpiFJ_An1UJB2RkDfX2k,5393
|
33
|
+
semantio/tools/web_browser.py,sha256=wqr5pj2GybkK9IHDb8C1BipS8ujV2l36WlwA8ZbKd88,9711
|
30
34
|
semantio/utils/__init__.py,sha256=Lx4X4iJpRhZzRmpQb80XXh5Ve8ZMOkadWAxXSmHpO_8,244
|
31
35
|
semantio/utils/config.py,sha256=ZTwUTqxjW3-w94zoU7GzivWyJe0JJGvBfuB4RUOuEs8,1198
|
32
36
|
semantio/utils/date_utils.py,sha256=x3oqRGv6ee_KCJ0LvCqqZh_FSgS6YGOHBwZQS4TJetY,1471
|
33
37
|
semantio/utils/file_utils.py,sha256=b_cMuJINEGk9ikNuNHSn9lsmICWwvtnCDZ03ndH_S2I,1779
|
34
38
|
semantio/utils/logger.py,sha256=TmGbP8BRjLMWjXi2GWzZ0RIXt70x9qX3FuIqghCNlwM,510
|
35
39
|
semantio/utils/validation_utils.py,sha256=iwoxEb4Q5ILqV6tbesMjPWPCCoL3AmPLejGUy6q8YvQ,1284
|
36
|
-
semantio-0.0.
|
37
|
-
semantio-0.0.
|
38
|
-
semantio-0.0.
|
39
|
-
semantio-0.0.
|
40
|
-
semantio-0.0.
|
41
|
-
semantio-0.0.
|
40
|
+
semantio-0.0.5.dist-info/LICENSE,sha256=mziLlfb9hZ8HKxm9V6BiHpmgJvmcDvswu1QBlDB-6vU,1074
|
41
|
+
semantio-0.0.5.dist-info/METADATA,sha256=PtDbsZ-tWXbte0RR40K5O_OklMKZiUsb-3dxGlmjklQ,6913
|
42
|
+
semantio-0.0.5.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92
|
43
|
+
semantio-0.0.5.dist-info/entry_points.txt,sha256=zbPgevSLwcLpdRHqI_atE8EOt8lK2vRF1AoDflDTo18,53
|
44
|
+
semantio-0.0.5.dist-info/top_level.txt,sha256=Yte_6mb-bh-I_lQwMjk1GijZkxPoX4Zmp3kBftC1ZlA,9
|
45
|
+
semantio-0.0.5.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|