Agent flows working

This commit is contained in:
Cyber MacGeddon 2025-04-22 20:01:35 +01:00
parent c9913297d2
commit 1d22d21152
23 changed files with 344 additions and 183 deletions

View file

@ -20,7 +20,11 @@ def output(text, prefix="> ", width=78):
) )
print(out) print(out)
p = AgentClient(pulsar_host="pulsar://localhost:6650") p = AgentClient(
pulsar_host="pulsar://pulsar:6650",
input_queue = "non-persistent://tg/request/agent:0000",
output_queue = "non-persistent://tg/response/agent:0000",
)
q = "How many cats does Mark have? Calculate that number raised to 0.4 power. Is that number lower than the numeric part of the mission identifier of the Space Shuttle Challenger on its last mission? If so, give me an apple pie recipe, otherwise return a poem about cheese." q = "How many cats does Mark have? Calculate that number raised to 0.4 power. Is that number lower than the numeric part of the mission identifier of the Space Shuttle Challenger on its last mission? If so, give me an apple pie recipe, otherwise return a poem about cheese."

View file

@ -10,9 +10,11 @@ rag = GraphRagClient(
output_queue = "non-persistent://tg/response/graph-rag:default", output_queue = "non-persistent://tg/response/graph-rag:default",
) )
query=""" #query="""
This knowledge graph describes the Space Shuttle disaster. #This knowledge graph describes the Space Shuttle disaster.
Present 20 facts which are present in the knowledge graph.""" #Present 20 facts which are present in the knowledge graph."""
query = "How many cats does Mark have?"
resp = rag.request(query) resp = rag.request(query)

View file

@ -26,4 +26,6 @@ from . document_embeddings_query_service import DocumentEmbeddingsQueryService
from . graph_embeddings_client import GraphEmbeddingsClientSpec from . graph_embeddings_client import GraphEmbeddingsClientSpec
from . triples_client import TriplesClientSpec from . triples_client import TriplesClientSpec
from . document_embeddings_client import DocumentEmbeddingsClientSpec from . document_embeddings_client import DocumentEmbeddingsClientSpec
from . agent_service import AgentService
from . graph_rag_client import GraphRagClientSpec

View file

@ -0,0 +1,39 @@
from . request_response_spec import RequestResponse, RequestResponseSpec
from .. schema import AgentRequest, AgentResponse
from .. knowledge import Uri, Literal
class AgentClient(RequestResponse):
async def request(self, recipient, question, plan=None, state=None,
history=[], timeout=300):
resp = await self.request(
AgentRequest(
question = question,
plan = plan,
state = state,
history = history,
),
recipient=recipient,
timeout=timeout,
)
print(resp, flush=True)
if resp.error:
raise RuntimeError(resp.error.message)
return resp
class GraphEmbeddingsClientSpec(RequestResponseSpec):
def __init__(
self, request_name, response_name,
):
super(GraphEmbeddingsClientSpec, self).__init__(
request_name = request_name,
request_schema = GraphEmbeddingsRequest,
response_name = response_name,
response_schema = GraphEmbeddingsResponse,
impl = GraphEmbeddingsClient,
)

View file

@ -0,0 +1,100 @@
"""
Agent manager service completion base class
"""
import time
from prometheus_client import Histogram
from .. schema import AgentRequest, AgentResponse, Error
from .. exceptions import TooManyRequests
from .. base import FlowProcessor, ConsumerSpec, ProducerSpec
default_ident = "agent-manager"
class AgentService(FlowProcessor):
def __init__(self, **params):
id = params.get("id")
super(AgentService, self).__init__(**params | { "id": id })
self.register_specification(
ConsumerSpec(
name = "request",
schema = AgentRequest,
handler = self.on_request
)
)
self.register_specification(
ProducerSpec(
name = "next",
schema = AgentRequest
)
)
self.register_specification(
ProducerSpec(
name = "response",
schema = AgentResponse
)
)
async def on_request(self, msg, consumer, flow):
try:
request = msg.value()
# Sender-produced ID
id = msg.properties()["id"]
async def respond(resp):
await flow("response").send(
resp,
properties={"id": id}
)
async def next(resp):
await flow("next").send(
resp,
properties={"id": id}
)
await self.agent_request(
request = request, respond = respond, next = next,
flow = flow
)
except TooManyRequests as e:
raise e
except Exception as e:
# Apart from rate limits, treat all exceptions as unrecoverable
print(f"on_request Exception: {e}")
print("Send error response...", flush=True)
await flow.producer["response"].send(
AgentResponse(
error=Error(
type = "agent-error",
message = str(e),
),
thought = None,
observation = None,
answer = None,
),
properties={"id": id}
)
@staticmethod
def add_args(parser):
FlowProcessor.add_args(parser)

View file

@ -89,7 +89,7 @@ class Consumer:
except Exception as e: except Exception as e:
print("Exception:", e, flush=True) print("consumer subs Exception:", e, flush=True)
await asyncio.sleep(self.reconnect_time) await asyncio.sleep(self.reconnect_time)
continue continue
@ -107,7 +107,7 @@ class Consumer:
except Exception as e: except Exception as e:
print("Exception:", e, flush=True) print("consumer loop exception:", e, flush=True)
self.consumer.close() self.consumer.close()
self.consumer = None self.consumer = None
await asyncio.sleep(self.reconnect_time) await asyncio.sleep(self.reconnect_time)
@ -184,7 +184,7 @@ class Consumer:
except Exception as e: except Exception as e:
print("Exception:", e, flush=True) print("consume exception:", e, flush=True)
# Message failed to be processed, this causes it to # Message failed to be processed, this causes it to
# be retried # be retried

View file

View file

0
trustgraph-base/trustgraph/base/embeddings_service.py Executable file → Normal file
View file

View file

@ -67,6 +67,11 @@ class FlowProcessor(AsyncProcessor):
# Get my flow config # Get my flow config
flow_config = json.loads(config["flows-active"][self.id]) flow_config = json.loads(config["flows-active"][self.id])
else:
print("No configuration settings for me.", flush=True)
flow_config = {}
# Get list of flows which should be running and are currently # Get list of flows which should be running and are currently
# running # running
wanted_flows = flow_config.keys() wanted_flows = flow_config.keys()
@ -84,10 +89,6 @@ class FlowProcessor(AsyncProcessor):
print("Handled config update") print("Handled config update")
else:
print("No configuration settings for me!", flush=True)
# Start threads, just call parent # Start threads, just call parent
async def start(self): async def start(self):
await super(FlowProcessor, self).start() await super(FlowProcessor, self).start()

View file

View file

View file

@ -0,0 +1,33 @@
from . request_response_spec import RequestResponse, RequestResponseSpec
from .. schema import GraphRagQuery, GraphRagResponse
class GraphRagClient(RequestResponse):
async def rag(self, query, user="trustgraph", collection="default",
timeout=600):
resp = await self.request(
GraphRagQuery(
query = query,
user = user,
collection = collection,
),
timeout=timeout
)
if resp.error:
raise RuntimeError(resp.error.message)
return resp.response
class GraphRagClientSpec(RequestResponseSpec):
def __init__(
self, request_name, response_name,
):
super(GraphRagClientSpec, self).__init__(
request_name = request_name,
request_schema = GraphRagQuery,
response_name = response_name,
response_schema = GraphRagResponse,
impl = GraphRagClient,
)

2
trustgraph-base/trustgraph/base/llm_service.py Executable file → Normal file
View file

@ -62,8 +62,6 @@ class LlmService(FlowProcessor):
id = msg.properties()["id"] id = msg.properties()["id"]
prompt = request.system + "\n\n" + request.prompt
with __class__.text_completion_metric.labels( with __class__.text_completion_metric.labels(
id=self.id, id=self.id,
flow=f"{flow.name}-{consumer.name}", flow=f"{flow.name}-{consumer.name}",

View file

@ -63,6 +63,22 @@ class PromptClient(RequestResponse):
timeout = timeout, timeout = timeout,
) )
async def agent_react(self, variables, timeout=600):
return await self.prompt(
id = "agent-react",
variables = variables,
timeout = timeout,
)
async def question(self, question, timeout=600):
return await self.prompt(
id = "question",
variables = {
"question": question,
},
timeout = timeout,
)
class PromptClientSpec(RequestResponseSpec): class PromptClientSpec(RequestResponseSpec):
def __init__( def __init__(
self, request_name, response_name, self, request_name, response_name,

View file

@ -40,7 +40,7 @@ class RequestResponse(Subscriber):
await self.producer.stop() await self.producer.stop()
await super(RequestResponse, self).stop() await super(RequestResponse, self).stop()
async def request(self, req, timeout=300): async def request(self, req, timeout=300, recipient=None):
id = str(uuid.uuid4()) id = str(uuid.uuid4())
@ -55,6 +55,16 @@ class RequestResponse(Subscriber):
properties={"id": id} properties={"id": id}
) )
except Exception as e:
print("Exception:", e)
raise e
try:
while True:
resp = await asyncio.wait_for( resp = await asyncio.wait_for(
q.get(), q.get(),
timeout=timeout timeout=timeout
@ -62,7 +72,23 @@ class RequestResponse(Subscriber):
print("Got response.", flush=True) print("Got response.", flush=True)
if recipient is None:
# If no recipient handler, just return the first
# response we get
return resp return resp
else:
# Recipient handler gets to decide when we're done b
# returning a boolean
fin = await recipient(resp)
# If done, return the last result otherwise loop round for
# next response
if fin:
return resp
else:
continue
except Exception as e: except Exception as e:
@ -108,4 +134,3 @@ class RequestResponseSpec(Spec):
flow.consumer[self.request_name] = rr flow.consumer[self.request_name] = rr

View file

View file

View file

@ -26,12 +26,5 @@ class AgentResponse(Record):
thought = String() thought = String()
observation = String() observation = String()
agent_request_queue = topic(
'agent', kind='non-persistent', namespace='request'
)
agent_response_queue = topic(
'agent', kind='non-persistent', namespace='response'
)
############################################################################ ############################################################################

View file

@ -8,12 +8,11 @@ logger = logging.getLogger(__name__)
class AgentManager: class AgentManager:
def __init__(self, context, tools, additional_context=None): def __init__(self, tools, additional_context=None):
self.context = context
self.tools = tools self.tools = tools
self.additional_context = additional_context self.additional_context = additional_context
def reason(self, question, history): async def reason(self, question, history, context):
tools = self.tools tools = self.tools
@ -56,10 +55,7 @@ class AgentManager:
logger.info(f"prompt: {variables}") logger.info(f"prompt: {variables}")
obj = self.context.prompt.request( obj = await context("prompt-request").agent_react(variables)
"agent-react",
variables
)
print(json.dumps(obj, indent=4), flush=True) print(json.dumps(obj, indent=4), flush=True)
@ -85,9 +81,13 @@ class AgentManager:
return a return a
async def react(self, question, history, think, observe): async def react(self, question, history, think, observe, context):
act = self.reason(question, history) act = await self.reason(
question = question,
history = history,
context = context,
)
logger.info(f"act: {act}") logger.info(f"act: {act}")
if isinstance(act, Final): if isinstance(act, Final):
@ -104,7 +104,12 @@ class AgentManager:
else: else:
raise RuntimeError(f"No action for {act.name}!") raise RuntimeError(f"No action for {act.name}!")
resp = action.implementation.invoke(**act.arguments) print("TOOL>>>", act)
resp = await action.implementation(context).invoke(
**act.arguments
)
print("RSETUL", resp)
resp = resp.strip() resp = resp.strip()

View file

@ -6,105 +6,68 @@ import json
import re import re
import sys import sys
from pulsar.schema import JsonSchema from ... base import AgentService, TextCompletionClientSpec, PromptClientSpec
from ... base import GraphRagClientSpec
from ... base import ConsumerProducer from ... schema import AgentRequest, AgentResponse, AgentStep, Error
from ... schema import Error
from ... schema import AgentRequest, AgentResponse, AgentStep
from ... schema import agent_request_queue, agent_response_queue
from ... schema import prompt_request_queue as pr_request_queue
from ... schema import prompt_response_queue as pr_response_queue
from ... schema import graph_rag_request_queue as gr_request_queue
from ... schema import graph_rag_response_queue as gr_response_queue
from ... clients.prompt_client import PromptClient
from ... clients.llm_client import LlmClient
from ... clients.graph_rag_client import GraphRagClient
from . tools import KnowledgeQueryImpl, TextCompletionImpl from . tools import KnowledgeQueryImpl, TextCompletionImpl
from . agent_manager import AgentManager from . agent_manager import AgentManager
from . types import Final, Action, Tool, Argument from . types import Final, Action, Tool, Argument
module = "agent" default_ident = "agent-manager"
default_max_iterations = 10
default_input_queue = agent_request_queue class Processor(AgentService):
default_output_queue = agent_response_queue
default_subscriber = module
default_max_iterations = 15
class Processor(ConsumerProducer):
def __init__(self, **params): def __init__(self, **params):
id = params.get("id")
self.max_iterations = int( self.max_iterations = int(
params.get("max_iterations", default_max_iterations) params.get("max_iterations", default_max_iterations)
) )
tools = {}
input_queue = params.get("input_queue", default_input_queue)
output_queue = params.get("output_queue", default_output_queue)
subscriber = params.get("subscriber", default_subscriber)
prompt_request_queue = params.get(
"prompt_request_queue", pr_request_queue
)
prompt_response_queue = params.get(
"prompt_response_queue", pr_response_queue
)
graph_rag_request_queue = params.get(
"graph_rag_request_queue", gr_request_queue
)
graph_rag_response_queue = params.get(
"graph_rag_response_queue", gr_response_queue
)
self.config_key = params.get("config_type", "agent") self.config_key = params.get("config_type", "agent")
super(Processor, self).__init__( super(Processor, self).__init__(
**params | { **params | {
"input_queue": input_queue, "id": id,
"output_queue": output_queue, "max_iterations": self.max_iterations,
"subscriber": subscriber, "config_type": self.config_key,
"input_schema": AgentRequest,
"output_schema": AgentResponse,
"prompt_request_queue": prompt_request_queue,
"prompt_response_queue": prompt_response_queue,
"graph_rag_request_queue": gr_request_queue,
"graph_rag_response_queue": gr_response_queue,
} }
) )
self.prompt = PromptClient(
subscriber=subscriber,
input_queue=prompt_request_queue,
output_queue=prompt_response_queue,
pulsar_host = self.pulsar_host,
pulsar_api_key=self.pulsar_api_key,
)
self.graph_rag = GraphRagClient(
subscriber=subscriber,
input_queue=graph_rag_request_queue,
output_queue=graph_rag_response_queue,
pulsar_host = self.pulsar_host,
pulsar_api_key=self.pulsar_api_key,
)
# Need to be able to feed requests to myself
self.recursive_input = self.client.create_producer(
topic=input_queue,
schema=JsonSchema(AgentRequest),
)
self.agent = AgentManager( self.agent = AgentManager(
context=self,
tools=[], tools=[],
additional_context="", additional_context="",
) )
self.config_handlers.append(self.on_config) self.config_handlers.append(self.on_tools_config)
async def on_config(self, version, config): self.register_specification(
TextCompletionClientSpec(
request_name = "text-completion-request",
response_name = "text-completion-response",
)
)
self.register_specification(
GraphRagClientSpec(
request_name = "graph-rag-request",
response_name = "graph-rag-response",
)
)
self.register_specification(
PromptClientSpec(
request_name = "prompt-request",
response_name = "prompt-response",
)
)
async def on_tools_config(self, config, version):
print("Loading configuration version", version) print("Loading configuration version", version)
@ -140,9 +103,9 @@ class Processor(ConsumerProducer):
impl_id = data.get("type") impl_id = data.get("type")
if impl_id == "knowledge-query": if impl_id == "knowledge-query":
impl = KnowledgeQueryImpl(self) impl = KnowledgeQueryImpl
elif impl_id == "text-completion": elif impl_id == "text-completion":
impl = TextCompletionImpl(self) impl = TextCompletionImpl
else: else:
raise RuntimeError( raise RuntimeError(
f"Tool-kind {impl_id} not known" f"Tool-kind {impl_id} not known"
@ -157,7 +120,6 @@ class Processor(ConsumerProducer):
) )
self.agent = AgentManager( self.agent = AgentManager(
context=self,
tools=tools, tools=tools,
additional_context=additional additional_context=additional
) )
@ -166,19 +128,14 @@ class Processor(ConsumerProducer):
except Exception as e: except Exception as e:
print("Exception:", e, flush=True) print("on_tools_config Exception:", e, flush=True)
print("Configuration reload failed", flush=True) print("Configuration reload failed", flush=True)
async def handle(self, msg): async def agent_request(self, request, respond, next, flow):
try: try:
v = msg.value() if request.history:
# Sender-produced ID
id = msg.properties()["id"]
if v.history:
history = [ history = [
Action( Action(
thought=h.thought, thought=h.thought,
@ -186,12 +143,12 @@ class Processor(ConsumerProducer):
arguments=h.arguments, arguments=h.arguments,
observation=h.observation observation=h.observation
) )
for h in v.history for h in request.history
] ]
else: else:
history = [] history = []
print(f"Question: {v.question}", flush=True) print(f"Question: {request.question}", flush=True)
if len(history) >= self.max_iterations: if len(history) >= self.max_iterations:
raise RuntimeError("Too many agent iterations") raise RuntimeError("Too many agent iterations")
@ -209,7 +166,7 @@ class Processor(ConsumerProducer):
observation=None, observation=None,
) )
await self.send(r, properties={"id": id}) await respond(r)
async def observe(x): async def observe(x):
@ -222,15 +179,21 @@ class Processor(ConsumerProducer):
observation=x, observation=x,
) )
await self.send(r, properties={"id": id}) await respond(r)
act = await self.agent.react(v.question, history, think, observe) act = await self.agent.react(
question = request.question,
history = history,
think = think,
observe = observe,
context = flow,
)
print(f"Action: {act}", flush=True) print(f"Action: {act}", flush=True)
print("Send response...", flush=True) if isinstance(act, Final):
if type(act) == Final: print("Send final response...", flush=True)
r = AgentResponse( r = AgentResponse(
answer=act.final, answer=act.final,
@ -238,18 +201,20 @@ class Processor(ConsumerProducer):
thought=None, thought=None,
) )
await self.send(r, properties={"id": id}) await respond(r)
print("Done.", flush=True) print("Done.", flush=True)
return return
print("Send next...", flush=True)
history.append(act) history.append(act)
r = AgentRequest( r = AgentRequest(
question=v.question, question=request.question,
plan=v.plan, plan=request.plan,
state=v.state, state=request.state,
history=[ history=[
AgentStep( AgentStep(
thought=h.thought, thought=h.thought,
@ -261,7 +226,7 @@ class Processor(ConsumerProducer):
] ]
) )
self.recursive_input.send(r, properties={"id": id}) await next(r)
print("Done.", flush=True) print("Done.", flush=True)
@ -269,7 +234,7 @@ class Processor(ConsumerProducer):
except Exception as e: except Exception as e:
print(f"Exception: {e}") print(f"agent_request Exception: {e}")
print("Send error response...", flush=True) print("Send error response...", flush=True)
@ -281,39 +246,12 @@ class Processor(ConsumerProducer):
response=None, response=None,
) )
await self.send(r, properties={"id": id}) await respond(r)
@staticmethod @staticmethod
def add_args(parser): def add_args(parser):
ConsumerProducer.add_args( AgentService.add_args(parser)
parser, default_input_queue, default_subscriber,
default_output_queue,
)
parser.add_argument(
'--prompt-request-queue',
default=pr_request_queue,
help=f'Prompt request queue (default: {pr_request_queue})',
)
parser.add_argument(
'--prompt-response-queue',
default=pr_response_queue,
help=f'Prompt response queue (default: {pr_response_queue})',
)
parser.add_argument(
'--graph-rag-request-queue',
default=gr_request_queue,
help=f'Graph RAG request queue (default: {gr_request_queue})',
)
parser.add_argument(
'--graph-rag-response-queue',
default=gr_response_queue,
help=f'Graph RAG response queue (default: {gr_response_queue})',
)
parser.add_argument( parser.add_argument(
'--max-iterations', '--max-iterations',
@ -329,5 +267,5 @@ class Processor(ConsumerProducer):
def run(): def run():
Processor.launch(module, __doc__) Processor.launch(default_ident, __doc__)

View file

@ -4,16 +4,22 @@
class KnowledgeQueryImpl: class KnowledgeQueryImpl:
def __init__(self, context): def __init__(self, context):
self.context = context self.context = context
def invoke(self, **arguments): async def invoke(self, **arguments):
return self.context.graph_rag.request(arguments.get("question")) client = self.context("graph-rag-request")
print("Graph RAG question...", flush=True)
return await client.rag(
arguments.get("question")
)
# This tool implementation knows how to do text completion. This uses # This tool implementation knows how to do text completion. This uses
# the prompt service, rather than talking to TextCompletion directly. # the prompt service, rather than talking to TextCompletion directly.
class TextCompletionImpl: class TextCompletionImpl:
def __init__(self, context): def __init__(self, context):
self.context = context self.context = context
def invoke(self, **arguments): async def invoke(self, **arguments):
return self.context.prompt.request( client = self.context("prompt-request")
"question", { "question": arguments.get("question") } print("Prompt question...", flush=True)
return await client.question(
arguments.get("question")
) )

View file

@ -39,4 +39,3 @@ class AgentRequestor(ServiceRequestor):
# The 2nd boolean expression indicates whether we're done responding # The 2nd boolean expression indicates whether we're done responding
return resp, (message.answer is not None) return resp, (message.answer is not None)