semantio 0.0.3__py3-none-any.whl → 0.0.5__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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
|