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.
@@ -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,3 @@
1
+ # Rustic AI Research Manager
2
+
3
+ Research workflow manager agent for Rustic AI guilds.
@@ -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,8 @@
1
+ from rustic_ai.research_manager.agent import (
2
+ ResearchManager,
3
+ ResearchManagerConf,
4
+ ResearchUpdates,
5
+ UserQuery,
6
+ )
7
+
8
+ __all__ = ["ResearchManager", "ResearchManagerConf", "ResearchUpdates", "UserQuery"]
@@ -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]))