versionhq 1.1.10.7__py3-none-any.whl → 1.1.10.9__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.
- versionhq/__init__.py +1 -1
- versionhq/_utils/vars.py +2 -0
- versionhq/agent/TEMPLATES/Backstory.py +2 -2
- versionhq/agent/default_agents.py +10 -0
- versionhq/agent/model.py +127 -39
- versionhq/agent/parser.py +3 -20
- versionhq/{_utils → agent}/rpm_controller.py +22 -15
- versionhq/knowledge/__init__.py +0 -0
- versionhq/knowledge/_utils.py +11 -0
- versionhq/knowledge/embedding.py +192 -0
- versionhq/knowledge/model.py +54 -0
- versionhq/knowledge/source.py +413 -0
- versionhq/knowledge/source_docling.py +129 -0
- versionhq/knowledge/storage.py +177 -0
- versionhq/llm/model.py +76 -62
- versionhq/memory/__init__.py +0 -0
- versionhq/memory/contextual_memory.py +96 -0
- versionhq/memory/model.py +174 -0
- versionhq/storage/base.py +14 -0
- versionhq/storage/ltm_sqlite_storage.py +131 -0
- versionhq/storage/mem0_storage.py +109 -0
- versionhq/storage/rag_storage.py +231 -0
- versionhq/storage/task_output_storage.py +18 -29
- versionhq/storage/utils.py +26 -0
- versionhq/task/TEMPLATES/Description.py +5 -0
- versionhq/task/evaluate.py +122 -0
- versionhq/task/model.py +134 -43
- versionhq/team/team_planner.py +1 -1
- versionhq/tool/model.py +44 -46
- {versionhq-1.1.10.7.dist-info → versionhq-1.1.10.9.dist-info}/METADATA +48 -39
- versionhq-1.1.10.9.dist-info/RECORD +64 -0
- versionhq-1.1.10.7.dist-info/RECORD +0 -45
- {versionhq-1.1.10.7.dist-info → versionhq-1.1.10.9.dist-info}/LICENSE +0 -0
- {versionhq-1.1.10.7.dist-info → versionhq-1.1.10.9.dist-info}/WHEEL +0 -0
- {versionhq-1.1.10.7.dist-info → versionhq-1.1.10.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,131 @@
|
|
1
|
+
import json
|
2
|
+
import sqlite3
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import Any, Dict, List, Optional
|
5
|
+
|
6
|
+
from versionhq._utils.logger import Logger
|
7
|
+
from versionhq.storage.utils import fetch_db_storage_path
|
8
|
+
|
9
|
+
|
10
|
+
class LTMSQLiteStorage:
|
11
|
+
"""
|
12
|
+
An updated SQLite storage class for LTM data storage.
|
13
|
+
"""
|
14
|
+
|
15
|
+
def __init__(self, db_path: Optional[str] = None) -> None:
|
16
|
+
if db_path is None:
|
17
|
+
db_path = str(Path(fetch_db_storage_path()) / "ltm_storage.db")
|
18
|
+
|
19
|
+
self.db_path = db_path
|
20
|
+
self._logger: Logger = Logger(verbose=True)
|
21
|
+
|
22
|
+
Path(self.db_path).parent.mkdir(parents=True, exist_ok=True)
|
23
|
+
self._initialize_db()
|
24
|
+
|
25
|
+
|
26
|
+
def _initialize_db(self):
|
27
|
+
"""
|
28
|
+
Initializes the SQLite database and creates LTM table
|
29
|
+
"""
|
30
|
+
try:
|
31
|
+
with sqlite3.connect(self.db_path) as conn:
|
32
|
+
cursor = conn.cursor()
|
33
|
+
cursor.execute(
|
34
|
+
"""
|
35
|
+
CREATE TABLE IF NOT EXISTS long_term_memories (
|
36
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
37
|
+
task_description TEXT,
|
38
|
+
metadata TEXT,
|
39
|
+
datetime TEXT,
|
40
|
+
score REAL
|
41
|
+
)
|
42
|
+
"""
|
43
|
+
)
|
44
|
+
|
45
|
+
conn.commit()
|
46
|
+
|
47
|
+
except sqlite3.Error as e:
|
48
|
+
self._logger.log(
|
49
|
+
level="error",
|
50
|
+
message=f"MEMORY ERROR: An error occurred during database initialization: {str(e)}",
|
51
|
+
color="red",
|
52
|
+
)
|
53
|
+
|
54
|
+
def save(self, task_description: str, metadata: Dict[str, Any], datetime: str, score: int | float) -> None:
|
55
|
+
"""
|
56
|
+
Saves data to the LTM table with error handling.
|
57
|
+
"""
|
58
|
+
try:
|
59
|
+
with sqlite3.connect(self.db_path) as conn:
|
60
|
+
cursor = conn.cursor()
|
61
|
+
cursor.execute(
|
62
|
+
"""
|
63
|
+
INSERT INTO long_term_memories (task_description, metadata, datetime, score)
|
64
|
+
VALUES (?, ?, ?, ?)
|
65
|
+
""",
|
66
|
+
(task_description, json.dumps(metadata), datetime, score),
|
67
|
+
)
|
68
|
+
conn.commit()
|
69
|
+
except sqlite3.Error as e:
|
70
|
+
self._logger.log(
|
71
|
+
level="error",
|
72
|
+
message=f"MEMORY ERROR: An error occurred while saving to LTM: {str(e)}",
|
73
|
+
color="red",
|
74
|
+
)
|
75
|
+
|
76
|
+
|
77
|
+
def load(self, task_description: str, latest_n: int) -> Optional[List[Dict[str, Any]]]:
|
78
|
+
"""
|
79
|
+
Queries the LTM table by task description with error handling.
|
80
|
+
"""
|
81
|
+
try:
|
82
|
+
with sqlite3.connect(self.db_path) as conn:
|
83
|
+
cursor = conn.cursor()
|
84
|
+
cursor.execute(
|
85
|
+
f"""
|
86
|
+
SELECT metadata, datetime, score
|
87
|
+
FROM long_term_memories
|
88
|
+
WHERE task_description = ?
|
89
|
+
ORDER BY datetime DESC, score ASC
|
90
|
+
LIMIT {latest_n}
|
91
|
+
""",
|
92
|
+
(task_description,),
|
93
|
+
)
|
94
|
+
rows = cursor.fetchall()
|
95
|
+
if rows:
|
96
|
+
return [
|
97
|
+
{
|
98
|
+
"metadata": json.loads(row[0]),
|
99
|
+
"datetime": row[1],
|
100
|
+
"score": row[2],
|
101
|
+
}
|
102
|
+
for row in rows
|
103
|
+
]
|
104
|
+
|
105
|
+
except sqlite3.Error as e:
|
106
|
+
self._logger.log(
|
107
|
+
level="error",
|
108
|
+
message=f"MEMORY ERROR: An error occurred while querying LTM: {e}",
|
109
|
+
color="red",
|
110
|
+
)
|
111
|
+
return None
|
112
|
+
|
113
|
+
|
114
|
+
def reset(self) -> None:
|
115
|
+
"""
|
116
|
+
Resets the LTM table with error handling.
|
117
|
+
"""
|
118
|
+
|
119
|
+
try:
|
120
|
+
with sqlite3.connect(self.db_path) as conn:
|
121
|
+
cursor = conn.cursor()
|
122
|
+
cursor.execute("DELETE FROM long_term_memories")
|
123
|
+
conn.commit()
|
124
|
+
|
125
|
+
except sqlite3.Error as e:
|
126
|
+
self._logger.log(
|
127
|
+
level="error",
|
128
|
+
message=f"MEMORY ERROR: An error occurred while deleting all rows in LTM: {str(e)}",
|
129
|
+
color="red",
|
130
|
+
)
|
131
|
+
return None
|
@@ -0,0 +1,109 @@
|
|
1
|
+
import os
|
2
|
+
from typing import Any, Dict, List
|
3
|
+
|
4
|
+
from mem0 import MemoryClient
|
5
|
+
|
6
|
+
from versionhq.storage.base import Storage
|
7
|
+
|
8
|
+
|
9
|
+
class Mem0Storage(Storage):
|
10
|
+
"""
|
11
|
+
Extends Storage to handle embedding and searching across entities using Mem0.
|
12
|
+
"""
|
13
|
+
|
14
|
+
def __init__(self, type, agent=None, user_id=None):
|
15
|
+
"""
|
16
|
+
Create a memory client using API keys and other config.
|
17
|
+
"""
|
18
|
+
|
19
|
+
super().__init__()
|
20
|
+
|
21
|
+
if type not in ["user", "stm", "ltm", "entities"]:
|
22
|
+
raise ValueError("Invalid type for Mem0Storage. Must be 'user' or 'agent'.")
|
23
|
+
|
24
|
+
self.memory_type = type
|
25
|
+
self.agent= agent
|
26
|
+
self.memory_config = agent.memory_config
|
27
|
+
|
28
|
+
user_id = user_id if user_id else self._get_user_id()
|
29
|
+
if type == "user" and not user_id:
|
30
|
+
raise ValueError("User ID is required for user memory type")
|
31
|
+
|
32
|
+
config = self.memory_config.get("config", {})
|
33
|
+
mem0_api_key = os.environ.get("MEM0_API_KEY", config.get("api_key"))
|
34
|
+
mem0_org_id = config.get("org_id")
|
35
|
+
mem0_project_id = config.get("project_id")
|
36
|
+
|
37
|
+
if mem0_org_id and mem0_project_id:
|
38
|
+
self.memory = MemoryClient(api_key=mem0_api_key, org_id=mem0_org_id, project_id=mem0_project_id)
|
39
|
+
else:
|
40
|
+
self.memory = MemoryClient(api_key=mem0_api_key)
|
41
|
+
|
42
|
+
|
43
|
+
def _sanitize_role(self, role: str) -> str:
|
44
|
+
"""
|
45
|
+
Sanitizes agent roles to ensure valid directory names.
|
46
|
+
"""
|
47
|
+
return role.replace("\n", "").replace(" ", "_").replace("/", "_")
|
48
|
+
|
49
|
+
|
50
|
+
def save(self, value: Any, metadata: Dict[str, Any]) -> None:
|
51
|
+
user_id = self._get_user_id()
|
52
|
+
agent_name = self._get_agent_name()
|
53
|
+
|
54
|
+
if self.memory_type == "user":
|
55
|
+
self.memory.add(value, user_id=user_id, metadata={**metadata})
|
56
|
+
|
57
|
+
elif self.memory_type == "stm":
|
58
|
+
agent_name = self._get_agent_name()
|
59
|
+
self.memory.add(value, agent_id=agent_name, metadata={"type": "stm", **metadata})
|
60
|
+
|
61
|
+
elif self.memory_type == "ltm":
|
62
|
+
agent_name = self._get_agent_name()
|
63
|
+
self.memory.add(value, agent_id=agent_name, infer=False, metadata={"type": "ltm", **metadata})
|
64
|
+
|
65
|
+
elif self.memory_type == "entities":
|
66
|
+
entity_name = self._get_agent_name()
|
67
|
+
self.memory.add(value, user_id=entity_name, metadata={"type": "entity", **metadata})
|
68
|
+
|
69
|
+
|
70
|
+
def search(self, query: str, limit: int = 3, score_threshold: float = 0.35) -> List[Any]:
|
71
|
+
params = {"query": query, "limit": limit}
|
72
|
+
|
73
|
+
if self.memory_type == "user":
|
74
|
+
user_id = self._get_user_id()
|
75
|
+
params["user_id"] = user_id
|
76
|
+
|
77
|
+
elif self.memory_type == "stm":
|
78
|
+
agent_name = self._get_agent_name()
|
79
|
+
params["agent_id"] = agent_name
|
80
|
+
params["metadata"] = {"type": "stm"}
|
81
|
+
|
82
|
+
elif self.memory_type == "ltm":
|
83
|
+
agent_name = self._get_agent_name()
|
84
|
+
params["agent_id"] = agent_name
|
85
|
+
params["metadata"] = {"type": "ltm"}
|
86
|
+
|
87
|
+
elif self.memory_type == "entities":
|
88
|
+
agent_name = self._get_agent_name()
|
89
|
+
params["agent_id"] = agent_name
|
90
|
+
params["metadata"] = {"type": "entity"}
|
91
|
+
|
92
|
+
results = self.memory.search(**params)
|
93
|
+
return [r for r in results if r["score"] >= score_threshold]
|
94
|
+
|
95
|
+
|
96
|
+
def _get_user_id(self):
|
97
|
+
if self.memory_type == "user":
|
98
|
+
if hasattr(self, "memory_config") and self.memory_config is not None:
|
99
|
+
return self.memory_config.get("config", {}).get("user_id")
|
100
|
+
else:
|
101
|
+
return None
|
102
|
+
return None
|
103
|
+
|
104
|
+
|
105
|
+
def _get_agent_name(self):
|
106
|
+
agents = self.agents if self.agents else []
|
107
|
+
agents = [self._sanitize_role(agent.role) for agent in agents]
|
108
|
+
agents = "_".join(agents)
|
109
|
+
return agents
|
@@ -0,0 +1,231 @@
|
|
1
|
+
import contextlib
|
2
|
+
import io
|
3
|
+
import logging
|
4
|
+
import os
|
5
|
+
import shutil
|
6
|
+
import uuid
|
7
|
+
from abc import ABC, abstractmethod
|
8
|
+
from typing import Any, Dict, List, Optional
|
9
|
+
|
10
|
+
from chromadb.api import ClientAPI
|
11
|
+
|
12
|
+
from versionhq.knowledge.embedding import EmbeddingConfigurator
|
13
|
+
from versionhq._utils.vars import MAX_FILE_NAME_LENGTH
|
14
|
+
from versionhq.storage.utils import fetch_db_storage_path
|
15
|
+
|
16
|
+
|
17
|
+
@contextlib.contextmanager
|
18
|
+
def suppress_logging(
|
19
|
+
logger_name="chromadb.segment.impl.vector.local_persistent_hnsw",
|
20
|
+
level=logging.ERROR,
|
21
|
+
):
|
22
|
+
logger = logging.getLogger(logger_name)
|
23
|
+
original_level = logger.getEffectiveLevel()
|
24
|
+
logger.setLevel(level)
|
25
|
+
with (
|
26
|
+
contextlib.redirect_stdout(io.StringIO()),
|
27
|
+
contextlib.redirect_stderr(io.StringIO()),
|
28
|
+
contextlib.suppress(UserWarning),
|
29
|
+
):
|
30
|
+
yield
|
31
|
+
logger.setLevel(original_level)
|
32
|
+
|
33
|
+
|
34
|
+
class BaseRAGStorage(ABC):
|
35
|
+
"""
|
36
|
+
Base class for RAG-based Storage implementations.
|
37
|
+
"""
|
38
|
+
|
39
|
+
app: Any | None = None
|
40
|
+
|
41
|
+
def __init__(
|
42
|
+
self,
|
43
|
+
type: str,
|
44
|
+
allow_reset: bool = True,
|
45
|
+
embedder_config: Optional[Any] = None,
|
46
|
+
agents: List[Any] = None,
|
47
|
+
):
|
48
|
+
self.type = type
|
49
|
+
self.allow_reset = allow_reset
|
50
|
+
self.embedder_config = embedder_config
|
51
|
+
self.agents = agents
|
52
|
+
|
53
|
+
def _initialize_agents(self) -> str:
|
54
|
+
if self.agents:
|
55
|
+
return "_".join(
|
56
|
+
[self._sanitize_role(agent.role) for agent in self.agents]
|
57
|
+
)
|
58
|
+
return ""
|
59
|
+
|
60
|
+
@abstractmethod
|
61
|
+
def _sanitize_role(self, role: str) -> str:
|
62
|
+
"""Sanitizes agent roles to ensure valid directory names."""
|
63
|
+
pass
|
64
|
+
|
65
|
+
@abstractmethod
|
66
|
+
def save(self, value: Any, metadata: Dict[str, Any]) -> None:
|
67
|
+
"""Save a value with metadata to the storage."""
|
68
|
+
pass
|
69
|
+
|
70
|
+
@abstractmethod
|
71
|
+
def search(self, query: str, limit: int = 3, filter: Optional[dict] = None, score_threshold: float = 0.35) -> List[Any]:
|
72
|
+
"""Search for entries in the storage."""
|
73
|
+
pass
|
74
|
+
|
75
|
+
@abstractmethod
|
76
|
+
def reset(self) -> None:
|
77
|
+
"""Reset the storage."""
|
78
|
+
pass
|
79
|
+
|
80
|
+
@abstractmethod
|
81
|
+
def _generate_embedding(self, text: str, metadata: Optional[Dict[str, Any]] = None) -> Any:
|
82
|
+
"""Generate an embedding for the given text and metadata."""
|
83
|
+
pass
|
84
|
+
|
85
|
+
@abstractmethod
|
86
|
+
def _initialize_app(self):
|
87
|
+
"""Initialize the vector db."""
|
88
|
+
pass
|
89
|
+
|
90
|
+
def setup_config(self, config: Dict[str, Any]):
|
91
|
+
"""Setup the config of the storage."""
|
92
|
+
pass
|
93
|
+
|
94
|
+
def initialize_client(self):
|
95
|
+
"""Initialize the client of the storage. This should setup the app and the db collection"""
|
96
|
+
pass
|
97
|
+
|
98
|
+
|
99
|
+
|
100
|
+
class RAGStorage(BaseRAGStorage):
|
101
|
+
"""
|
102
|
+
Extends Storage to handle embeddings for memory entries, improving
|
103
|
+
search efficiency.
|
104
|
+
"""
|
105
|
+
|
106
|
+
app: ClientAPI | None = None
|
107
|
+
|
108
|
+
def __init__(self, type, allow_reset=True, embedder_config=None, agents=list(), path=None):
|
109
|
+
super().__init__(type, allow_reset, embedder_config, agents)
|
110
|
+
agents = agents
|
111
|
+
agents = [self._sanitize_role(agent.role) for agent in agents]
|
112
|
+
agents = "_".join(agents)
|
113
|
+
|
114
|
+
self.agents = agents
|
115
|
+
self.storage_file_name = self._build_storage_file_name(type, agents)
|
116
|
+
self.type = type
|
117
|
+
self.allow_reset = allow_reset
|
118
|
+
self.path = path
|
119
|
+
self._initialize_app()
|
120
|
+
|
121
|
+
|
122
|
+
def _set_embedder_config(self):
|
123
|
+
configurator = EmbeddingConfigurator()
|
124
|
+
self.embedder_config = configurator.configure_embedder(self.embedder_config)
|
125
|
+
|
126
|
+
|
127
|
+
def _initialize_app(self) -> None:
|
128
|
+
import chromadb
|
129
|
+
from chromadb.config import Settings
|
130
|
+
|
131
|
+
self._set_embedder_config()
|
132
|
+
chroma_client = chromadb.PersistentClient(
|
133
|
+
path=self.path if self.path else self.storage_file_name,
|
134
|
+
settings=Settings(allow_reset=self.allow_reset),
|
135
|
+
)
|
136
|
+
self.app = chroma_client
|
137
|
+
|
138
|
+
try:
|
139
|
+
self.collection = self.app.get_collection(name=self.type, embedding_function=self.embedder_config)
|
140
|
+
except Exception:
|
141
|
+
self.collection = self.app.create_collection(name=self.type, embedding_function=self.embedder_config)
|
142
|
+
|
143
|
+
|
144
|
+
def _sanitize_role(self, role: str) -> str:
|
145
|
+
"""
|
146
|
+
Sanitizes agent roles to ensure valid directory names.
|
147
|
+
"""
|
148
|
+
return role.replace("\n", "").replace(" ", "_").replace("/", "_")
|
149
|
+
|
150
|
+
|
151
|
+
def _build_storage_file_name(self, type: str, file_name: str) -> str:
|
152
|
+
"""
|
153
|
+
Ensures file name does not exceed max allowed by OS
|
154
|
+
"""
|
155
|
+
base_path = f"{fetch_db_storage_path()}/{type}"
|
156
|
+
|
157
|
+
if len(file_name) > MAX_FILE_NAME_LENGTH:
|
158
|
+
logging.warning(
|
159
|
+
f"Trimming file name from {len(file_name)} to {MAX_FILE_NAME_LENGTH} characters."
|
160
|
+
)
|
161
|
+
file_name = file_name[:MAX_FILE_NAME_LENGTH]
|
162
|
+
|
163
|
+
return f"{base_path}/{file_name}"
|
164
|
+
|
165
|
+
|
166
|
+
def save(self, value: Any, metadata: Dict[str, Any]) -> None:
|
167
|
+
if not hasattr(self, "app") or not hasattr(self, "collection"):
|
168
|
+
self._initialize_app()
|
169
|
+
try:
|
170
|
+
self._generate_embedding(value, metadata)
|
171
|
+
except Exception as e:
|
172
|
+
logging.error(f"Error during {self.type} save: {str(e)}")
|
173
|
+
|
174
|
+
|
175
|
+
def search(self, query: str, limit: int = 3, filter: Optional[dict] = None, score_threshold: float = 0.35) -> List[Any]:
|
176
|
+
if not hasattr(self, "app"):
|
177
|
+
self._initialize_app()
|
178
|
+
|
179
|
+
try:
|
180
|
+
with suppress_logging():
|
181
|
+
response = self.collection.query(query_texts=query, n_results=limit)
|
182
|
+
|
183
|
+
results = []
|
184
|
+
for i in range(len(response["ids"][0])):
|
185
|
+
result = {
|
186
|
+
"id": response["ids"][0][i],
|
187
|
+
"metadata": response["metadatas"][0][i],
|
188
|
+
"context": response["documents"][0][i],
|
189
|
+
"score": response["distances"][0][i],
|
190
|
+
}
|
191
|
+
if result["score"] >= score_threshold:
|
192
|
+
results.append(result)
|
193
|
+
|
194
|
+
return results
|
195
|
+
except Exception as e:
|
196
|
+
logging.error(f"Error during {self.type} search: {str(e)}")
|
197
|
+
return []
|
198
|
+
|
199
|
+
|
200
|
+
def _generate_embedding(self, text: str, metadata: Dict[str, Any]) -> None:
|
201
|
+
if not hasattr(self, "app") or not hasattr(self, "collection"):
|
202
|
+
self._initialize_app()
|
203
|
+
|
204
|
+
self.collection.add(
|
205
|
+
documents=[text],
|
206
|
+
metadatas=[metadata or {}],
|
207
|
+
ids=[str(uuid.uuid4())],
|
208
|
+
)
|
209
|
+
|
210
|
+
|
211
|
+
def reset(self) -> None:
|
212
|
+
try:
|
213
|
+
if self.app:
|
214
|
+
self.app.reset()
|
215
|
+
shutil.rmtree(f"{fetch_db_storage_path()}/{self.type}")
|
216
|
+
self.app = None
|
217
|
+
self.collection = None
|
218
|
+
except Exception as e:
|
219
|
+
if "attempt to write a readonly database" in str(e):
|
220
|
+
pass
|
221
|
+
else:
|
222
|
+
raise Exception(f"An error occurred while resetting the {self.type} memory: {e}")
|
223
|
+
|
224
|
+
def _create_default_embedding_function(self):
|
225
|
+
from chromadb.utils.embedding_functions.openai_embedding_function import (
|
226
|
+
OpenAIEmbeddingFunction,
|
227
|
+
)
|
228
|
+
|
229
|
+
return OpenAIEmbeddingFunction(
|
230
|
+
api_key=os.getenv("OPENAI_API_KEY"), model_name="text-embedding-3-small"
|
231
|
+
)
|
@@ -1,21 +1,10 @@
|
|
1
|
-
import appdirs
|
2
|
-
import os
|
3
1
|
import json
|
4
2
|
import sqlite3
|
5
3
|
import datetime
|
6
4
|
from typing import Any, Dict, List, Optional
|
7
|
-
from dotenv import load_dotenv
|
8
|
-
from pathlib import Path
|
9
5
|
|
10
6
|
from versionhq._utils.logger import Logger
|
11
|
-
|
12
|
-
load_dotenv(override=True)
|
13
|
-
|
14
|
-
def fetch_db_storage_path():
|
15
|
-
directory_name = Path.cwd().name
|
16
|
-
data_dir = Path(appdirs.user_data_dir(appname=directory_name, appauthor="Version IO Sdn Bhd.", version=None, roaming=False))
|
17
|
-
data_dir.mkdir(parents=True, exist_ok=True)
|
18
|
-
return data_dir
|
7
|
+
from versionhq.storage.utils import fetch_db_storage_path
|
19
8
|
|
20
9
|
storage_path = fetch_db_storage_path()
|
21
10
|
default_db_name = "task_outputs"
|
@@ -37,25 +26,25 @@ class TaskOutputSQLiteStorage:
|
|
37
26
|
Initializes the SQLite database and creates LTM table.
|
38
27
|
"""
|
39
28
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
29
|
+
try:
|
30
|
+
with sqlite3.connect(self.db_path) as conn:
|
31
|
+
cursor = conn.cursor()
|
32
|
+
cursor.execute(
|
33
|
+
"""
|
34
|
+
CREATE TABLE IF NOT EXISTS task_outputs (
|
35
|
+
task_id TEXT PRIMARY KEY,
|
36
|
+
output JSON,
|
37
|
+
task_index INTEGER,
|
38
|
+
inputs JSON,
|
39
|
+
was_replayed BOOLEAN,
|
40
|
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
41
|
+
)
|
44
42
|
"""
|
45
|
-
CREATE TABLE IF NOT EXISTS task_outputs (
|
46
|
-
task_id TEXT PRIMARY KEY,
|
47
|
-
output JSON,
|
48
|
-
task_index INTEGER,
|
49
|
-
inputs JSON,
|
50
|
-
was_replayed BOOLEAN,
|
51
|
-
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
52
43
|
)
|
53
|
-
|
54
|
-
)
|
55
|
-
conn.commit()
|
44
|
+
conn.commit()
|
56
45
|
|
57
|
-
|
58
|
-
|
46
|
+
except sqlite3.Error as e:
|
47
|
+
self._logger.log(level="error", message=f"SQL database initialization failed: {str(e)}", color="red")
|
59
48
|
|
60
49
|
|
61
50
|
def add(self, task, output: Dict[str, Any], task_index: int, was_replayed: bool = False, inputs: Dict[str, Any] = {}):
|
@@ -91,7 +80,7 @@ class TaskOutputSQLiteStorage:
|
|
91
80
|
|
92
81
|
if cursor.rowcount == 0:
|
93
82
|
self._logger.log(
|
94
|
-
level="
|
83
|
+
level="warning", message=f"No row found with task_index {task_index}. No update performed.", color="yellow",
|
95
84
|
)
|
96
85
|
|
97
86
|
except sqlite3.Error as e:
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import appdirs
|
2
|
+
import os
|
3
|
+
from dotenv import load_dotenv
|
4
|
+
from pathlib import Path
|
5
|
+
|
6
|
+
load_dotenv(override=True)
|
7
|
+
|
8
|
+
def fetch_db_storage_path() -> str:
|
9
|
+
directory_name = get_project_directory_name()
|
10
|
+
data_dir = Path(appdirs.user_data_dir(appname=directory_name, appauthor="Version IO Sdn Bhd", version=None, roaming=False))
|
11
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
12
|
+
return str(data_dir)
|
13
|
+
|
14
|
+
|
15
|
+
def get_project_directory_name() -> str:
|
16
|
+
"""
|
17
|
+
Returns the current project directory name
|
18
|
+
"""
|
19
|
+
project_directory_name = os.environ.get("STORAGE_DIR")
|
20
|
+
|
21
|
+
if project_directory_name:
|
22
|
+
return project_directory_name
|
23
|
+
else:
|
24
|
+
cwd = Path.cwd()
|
25
|
+
project_directory_name = cwd.name
|
26
|
+
return project_directory_name
|
@@ -0,0 +1,5 @@
|
|
1
|
+
EVALUATE="""Evaluate the accuracy and quality of the following task output. For each criterion defined below, provide a score between 0 (completely inaccurate/unacceptable) and 1 (perfectly accurate/acceptable), and suggest specific improvements to address any shortcomings.
|
2
|
+
Task: {task_description}
|
3
|
+
Task Output: {task_output}
|
4
|
+
Evaluation criteria: {eval_criteria}
|
5
|
+
"""
|
@@ -0,0 +1,122 @@
|
|
1
|
+
from typing import List, Optional, Dict, Any
|
2
|
+
from typing_extensions import Self
|
3
|
+
|
4
|
+
from pydantic import BaseModel, Field, InstanceOf, model_validator
|
5
|
+
|
6
|
+
"""
|
7
|
+
Evaluate task output from accuracy, token consumption, latency perspectives, and mark the score from 0 to 1.
|
8
|
+
"""
|
9
|
+
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
class ScoreFormat:
|
14
|
+
def __init__(self, rate: float | int = 0, weight: int = 1):
|
15
|
+
self.rate = rate
|
16
|
+
self.weight = weight
|
17
|
+
self.aggregate = rate * weight
|
18
|
+
|
19
|
+
|
20
|
+
class Score:
|
21
|
+
"""
|
22
|
+
Evaluate the score on 0 (no performance) to 1 scale.
|
23
|
+
`rate`: Any float from 0.0 to 1.0 given by an agent.
|
24
|
+
`weight`: Importance of each factor to the aggregated score.
|
25
|
+
"""
|
26
|
+
|
27
|
+
def __init__(
|
28
|
+
self,
|
29
|
+
brand_tone: ScoreFormat = ScoreFormat(0, 0),
|
30
|
+
audience: ScoreFormat = ScoreFormat(0, 0),
|
31
|
+
track_record: ScoreFormat = ScoreFormat(0, 0),
|
32
|
+
config: Optional[Dict[str, ScoreFormat]] = None
|
33
|
+
):
|
34
|
+
self.brand_tone = brand_tone
|
35
|
+
self.audience = audience
|
36
|
+
self.track_record = track_record
|
37
|
+
self.config = config
|
38
|
+
|
39
|
+
if self.config:
|
40
|
+
for k, v in self.config.items():
|
41
|
+
if isinstance(v, ScoreFormat):
|
42
|
+
setattr(self, k, v)
|
43
|
+
|
44
|
+
|
45
|
+
def result(self) -> int:
|
46
|
+
aggregate_score, denominator = 0, 0
|
47
|
+
|
48
|
+
for k, v in self.__dict__.items():
|
49
|
+
aggregate_score += v.aggregate
|
50
|
+
denominator += v.weight
|
51
|
+
|
52
|
+
if denominator == 0:
|
53
|
+
return 0
|
54
|
+
|
55
|
+
return round(aggregate_score / denominator, 2)
|
56
|
+
|
57
|
+
|
58
|
+
class EvaluationItem(BaseModel):
|
59
|
+
"""
|
60
|
+
A class to store evaluation and suggestion by the given criteria such as accuracy.
|
61
|
+
"""
|
62
|
+
criteria: str
|
63
|
+
suggestion: str
|
64
|
+
score: int | float
|
65
|
+
|
66
|
+
def _convert_score_to_score_format(self, weight: int = 1) -> ScoreFormat | None:
|
67
|
+
if self.score and isinstance(self.score, (int, float)):
|
68
|
+
return ScoreFormat(rate=self.score, weight=weight)
|
69
|
+
|
70
|
+
else: return None
|
71
|
+
|
72
|
+
|
73
|
+
|
74
|
+
class Evaluation(BaseModel):
|
75
|
+
# expected_outcome: Optional[str] = Field(default=None, description="human input on expected outcome")
|
76
|
+
items: List[EvaluationItem] = []
|
77
|
+
latency: int = Field(default=None, description="seconds")
|
78
|
+
tokens: int = Field(default=None, description="tokens consumed")
|
79
|
+
responsible_agent: Any = Field(default=None, description="store agent instance that evaluates the outcome")
|
80
|
+
|
81
|
+
@model_validator(mode="after")
|
82
|
+
def set_up_responsible_agent(self) -> Self:
|
83
|
+
from versionhq.agent.default_agents import task_evaluator
|
84
|
+
self.responsible_agent = task_evaluator
|
85
|
+
return self
|
86
|
+
|
87
|
+
|
88
|
+
@property
|
89
|
+
def aggregate_score(self) -> float:
|
90
|
+
"""
|
91
|
+
Calcurate aggregate score from evaluation items.
|
92
|
+
"""
|
93
|
+
if not self.items:
|
94
|
+
return 0
|
95
|
+
|
96
|
+
aggregate_score = 0
|
97
|
+
denominator = 0
|
98
|
+
|
99
|
+
for item in self.items:
|
100
|
+
score_format = item._convert_score_to_score_format()
|
101
|
+
aggregate_score += score_format.aggregate if score_format else 0
|
102
|
+
denominator += score_format.weight if score_format else 0
|
103
|
+
|
104
|
+
if denominator == 0:
|
105
|
+
return 0
|
106
|
+
|
107
|
+
return round(aggregate_score / denominator, 2)
|
108
|
+
|
109
|
+
|
110
|
+
@property
|
111
|
+
def suggestion_summary(self) -> str | None:
|
112
|
+
"""
|
113
|
+
Return a summary of the suggestions
|
114
|
+
"""
|
115
|
+
if not self.items:
|
116
|
+
return None
|
117
|
+
|
118
|
+
summary = ""
|
119
|
+
for item in self.items:
|
120
|
+
summary += f"{item.suggestion}, "
|
121
|
+
|
122
|
+
return summary
|