rusticai-research-manager 1.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rusticai_research_manager-1.4.0/PKG-INFO +24 -0
- rusticai_research_manager-1.4.0/README.md +3 -0
- rusticai_research_manager-1.4.0/pyproject.toml +42 -0
- rusticai_research_manager-1.4.0/src/rustic_ai/research_manager/__init__.py +8 -0
- rusticai_research_manager-1.4.0/src/rustic_ai/research_manager/agent.py +167 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rusticai-research-manager
|
|
3
|
+
Version: 1.4.0
|
|
4
|
+
Summary: Research workflow manager agent for Rustic AI guilds
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Author: Dragonscale Industries Inc.
|
|
7
|
+
Author-email: dev@dragonscale.ai
|
|
8
|
+
Requires-Python: >=3.13,<3.14
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Provides-Extra: test
|
|
12
|
+
Requires-Dist: rusticai-core (>=1.4.0,<1.5.0)
|
|
13
|
+
Requires-Dist: rusticai-playwright (>=1.3.1,<1.4.0)
|
|
14
|
+
Requires-Dist: rusticai-serpapi (>=1.3.1,<1.4.0)
|
|
15
|
+
Requires-Dist: shortuuid (>=1.0.13,<2.0.0)
|
|
16
|
+
Project-URL: Homepage, https://www.rustic.ai/
|
|
17
|
+
Project-URL: Repository, https://github.com/rustic-ai/rustic-ai
|
|
18
|
+
Project-URL: Rustic AI Core, https://pypi.org/project/rusticai-core/
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Rustic AI Research Manager
|
|
22
|
+
|
|
23
|
+
Research workflow manager agent for Rustic AI guilds.
|
|
24
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["poetry-core"]
|
|
3
|
+
build-backend = "poetry.core.masonry.api"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rusticai-research-manager"
|
|
7
|
+
version = "1.4.0"
|
|
8
|
+
description = "Research workflow manager agent for Rustic AI guilds"
|
|
9
|
+
authors = [{name = "Dragonscale Industries Inc.", email = "dev@dragonscale.ai"}]
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.13,<3.14"
|
|
13
|
+
dynamic = ["dependencies"]
|
|
14
|
+
urls = { Homepage = "https://www.rustic.ai/", Repository = "https://github.com/rustic-ai/rustic-ai", "Rustic AI Core" = "https://pypi.org/project/rusticai-core/" }
|
|
15
|
+
|
|
16
|
+
[tool.poetry]
|
|
17
|
+
packages = [{ include = "rustic_ai", from = "src" }]
|
|
18
|
+
|
|
19
|
+
[tool.poetry.dependencies]
|
|
20
|
+
rusticai-core = { version = "1.4.0"}
|
|
21
|
+
rusticai-playwright = { version = "1.3.1"}
|
|
22
|
+
rusticai-serpapi = { version = "1.3.1"}
|
|
23
|
+
shortuuid = "^1.0.13"
|
|
24
|
+
|
|
25
|
+
[tool.poetry-monorepo.deps]
|
|
26
|
+
|
|
27
|
+
[tool.poetry.group.dev]
|
|
28
|
+
optional = true
|
|
29
|
+
|
|
30
|
+
[tool.poetry.group.dev.dependencies]
|
|
31
|
+
pytest = "^9.1.1"
|
|
32
|
+
rusticai-testing = { version = "1.3.0"}
|
|
33
|
+
|
|
34
|
+
[tool.poetry.extras]
|
|
35
|
+
test = ["pytest", "rusticai-testing"]
|
|
36
|
+
|
|
37
|
+
[tool.black]
|
|
38
|
+
line-length = 120
|
|
39
|
+
target-version = ["py313"]
|
|
40
|
+
|
|
41
|
+
[tool.isort]
|
|
42
|
+
profile = "black"
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Research workflow manager agent."""
|
|
2
|
+
|
|
3
|
+
import textwrap
|
|
4
|
+
from typing import Dict
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
import shortuuid
|
|
8
|
+
|
|
9
|
+
from rustic_ai.core import Agent, AgentTag
|
|
10
|
+
from rustic_ai.core.agents.indexing.vector_agent import VectorSearchQuery
|
|
11
|
+
from rustic_ai.core.guild import BaseAgentProps, agent
|
|
12
|
+
from rustic_ai.core.guild.agent import ProcessContext
|
|
13
|
+
from rustic_ai.core.guild.agent_ext.depends.llm import LLM
|
|
14
|
+
from rustic_ai.core.guild.agent_ext.depends.llm.models import (
|
|
15
|
+
ArrayOfContentParts,
|
|
16
|
+
ChatCompletionRequest,
|
|
17
|
+
SystemMessage,
|
|
18
|
+
TextContentPart,
|
|
19
|
+
UserMessage,
|
|
20
|
+
)
|
|
21
|
+
from rustic_ai.core.guild.agent_ext.depends.vectorstore import VectorSearchResults
|
|
22
|
+
from rustic_ai.core.guild.metaprog.agent_registry import AgentDependency
|
|
23
|
+
from rustic_ai.core.ui_protocol.types import TextFormat
|
|
24
|
+
from rustic_ai.playwright.agent import WebScrapingCompleted
|
|
25
|
+
from rustic_ai.serpapi.agent import SERPQuery
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class UserQuery(BaseModel):
|
|
29
|
+
id: str = Field(default_factory=shortuuid.uuid)
|
|
30
|
+
query: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ResearchUpdates(BaseModel):
|
|
34
|
+
id: str
|
|
35
|
+
query: str
|
|
36
|
+
update: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ResearchManagerConf(BaseAgentProps):
|
|
40
|
+
serp_results: int = Field(default=5)
|
|
41
|
+
vector_results: int = Field(default=5)
|
|
42
|
+
max_serp_attempts: int = Field(default=2)
|
|
43
|
+
context_max: int = Field(default=100000)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ResearchManager(Agent[ResearchManagerConf]):
|
|
47
|
+
"""
|
|
48
|
+
Agent that manages the research process.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self):
|
|
52
|
+
self.serp_results = self.config.serp_results
|
|
53
|
+
self.vector_results = self.config.vector_results
|
|
54
|
+
self.max_serp_attempts = self.config.max_serp_attempts
|
|
55
|
+
self.context_max = self.config.context_max
|
|
56
|
+
|
|
57
|
+
self.queries: Dict[str, str] = {}
|
|
58
|
+
self.query_status: Dict[str, str] = {}
|
|
59
|
+
self.query_users: Dict[str, AgentTag] = {}
|
|
60
|
+
self.query_attempts: Dict[str, int] = {}
|
|
61
|
+
|
|
62
|
+
def _process_query(self, query_id: str, query: str, ctx: ProcessContext):
|
|
63
|
+
"""
|
|
64
|
+
Process the user query.
|
|
65
|
+
"""
|
|
66
|
+
self.queries[query_id] = query
|
|
67
|
+
|
|
68
|
+
if ctx.routing_origin.origin_sender:
|
|
69
|
+
self.query_users[query_id] = ctx.routing_origin.origin_sender
|
|
70
|
+
|
|
71
|
+
self.query_attempts[query_id] = 0
|
|
72
|
+
ctx.send(VectorSearchQuery(query=query, id=query_id, k=self.vector_results))
|
|
73
|
+
self.query_status[query_id] = "Searching vector index"
|
|
74
|
+
ctx.send(ResearchUpdates(id=query_id, query=query, update=self.query_status[query_id]))
|
|
75
|
+
|
|
76
|
+
@agent.processor(ChatCompletionRequest)
|
|
77
|
+
def process_user_query(self, ctx: ProcessContext[ChatCompletionRequest]):
|
|
78
|
+
"""
|
|
79
|
+
Run the user query against the vector index.
|
|
80
|
+
"""
|
|
81
|
+
messages = ctx.payload.messages
|
|
82
|
+
user_messages = [msg.content for msg in messages if isinstance(msg, UserMessage)]
|
|
83
|
+
question = ""
|
|
84
|
+
for u_msg in user_messages:
|
|
85
|
+
if isinstance(u_msg, ArrayOfContentParts):
|
|
86
|
+
for msg_part in u_msg:
|
|
87
|
+
content = msg_part[1]
|
|
88
|
+
text_msgs = [msg.text for msg in content if isinstance(msg, TextContentPart)]
|
|
89
|
+
question += "".join(text_msgs)
|
|
90
|
+
elif isinstance(u_msg, str):
|
|
91
|
+
question = u_msg
|
|
92
|
+
if len(question) > 0:
|
|
93
|
+
query: UserQuery = UserQuery(query=question)
|
|
94
|
+
self._process_query(query.id, query.query, ctx)
|
|
95
|
+
else:
|
|
96
|
+
ctx.send(TextFormat(text="No results found"))
|
|
97
|
+
|
|
98
|
+
@agent.processor(VectorSearchResults, depends_on=[AgentDependency(dependency_key="llm", guild_level=True)])
|
|
99
|
+
def process_vector_results(self, ctx: ProcessContext[VectorSearchResults], llm: LLM):
|
|
100
|
+
"""
|
|
101
|
+
Process the vector search results.
|
|
102
|
+
"""
|
|
103
|
+
data: VectorSearchResults = ctx.payload
|
|
104
|
+
query_id = data.query_id
|
|
105
|
+
|
|
106
|
+
if data.documents:
|
|
107
|
+
# If vector search had results are found, generate a response using the LLM model
|
|
108
|
+
context = "\n".join([doc.content for doc in data.documents])
|
|
109
|
+
|
|
110
|
+
if len(context) > self.context_max:
|
|
111
|
+
context = context[: self.context_max]
|
|
112
|
+
|
|
113
|
+
self.query_status[query_id] = "Generating response"
|
|
114
|
+
ctx.send(ResearchUpdates(id=query_id, query=self.queries[query_id], update=self.query_status[query_id]))
|
|
115
|
+
|
|
116
|
+
completion_request = ChatCompletionRequest(
|
|
117
|
+
messages=[
|
|
118
|
+
SystemMessage(content=textwrap.dedent("""You are a helpful question-answering assistant.
|
|
119
|
+
User will ask you question and will provide you the context of the question.
|
|
120
|
+
Use the context to answer the question. You need to provide the answer to the question based on the context provided.
|
|
121
|
+
If the context doesn't contain the answer, just say that you don't know.
|
|
122
|
+
Use three sentences maximum and keep the answer concise.""")),
|
|
123
|
+
UserMessage(
|
|
124
|
+
content=f"Question: {data.query}\nContext: {context}",
|
|
125
|
+
name=self.query_users[query_id].id,
|
|
126
|
+
),
|
|
127
|
+
],
|
|
128
|
+
)
|
|
129
|
+
response = llm.completion(completion_request)
|
|
130
|
+
self.query_status[query_id] = "Response generated"
|
|
131
|
+
if response.choices and response.choices[0].message:
|
|
132
|
+
ctx.send(TextFormat(text=response.choices[0].message.content))
|
|
133
|
+
|
|
134
|
+
elif self.query_attempts[query_id] >= self.max_serp_attempts:
|
|
135
|
+
# If the maximum number of serp attempts is reached, return no results
|
|
136
|
+
self.query_status[query_id] = "No results found"
|
|
137
|
+
ctx.send(ResearchUpdates(id=query_id, query=self.queries[query_id], update=self.query_status[query_id]))
|
|
138
|
+
ctx.send(TextFormat(text="No results found"))
|
|
139
|
+
|
|
140
|
+
else:
|
|
141
|
+
# If no results are found, start serp search and scrape the responses
|
|
142
|
+
self.query_attempts[query_id] += 1
|
|
143
|
+
serp_query = SERPQuery(
|
|
144
|
+
engine="google",
|
|
145
|
+
query=self.queries[query_id],
|
|
146
|
+
num=self.serp_results,
|
|
147
|
+
id=query_id,
|
|
148
|
+
start=self.query_attempts[query_id] * self.serp_results,
|
|
149
|
+
)
|
|
150
|
+
ctx.send(serp_query)
|
|
151
|
+
self.query_status[query_id] = "Searching SERP API"
|
|
152
|
+
ctx.send(ResearchUpdates(id=query_id, query=self.queries[query_id], update=self.query_status[query_id]))
|
|
153
|
+
|
|
154
|
+
@agent.processor(WebScrapingCompleted)
|
|
155
|
+
def process_scraping_completion(self, ctx: ProcessContext[WebScrapingCompleted]):
|
|
156
|
+
"""
|
|
157
|
+
Process the scraping completion.
|
|
158
|
+
"""
|
|
159
|
+
data: WebScrapingCompleted = ctx.payload
|
|
160
|
+
query_id = data.id
|
|
161
|
+
query = self.queries[query_id]
|
|
162
|
+
|
|
163
|
+
self.query_status[query_id] = "Scraping completed"
|
|
164
|
+
ctx.send(ResearchUpdates(id=data.id, query=query, update=self.query_status[data.id]))
|
|
165
|
+
ctx.send(VectorSearchQuery(query=query, id=query_id, k=self.vector_results))
|
|
166
|
+
self.query_status[data.id] = "Searching vector index again"
|
|
167
|
+
ctx.send(ResearchUpdates(id=data.id, query=query, update=self.query_status[data.id]))
|