aap-core 0.1.3.dev1__tar.gz → 0.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: aap-core
3
- Version: 0.1.3.dev1
3
+ Version: 0.2.0
4
4
  Summary: Agent design pattern core framework
5
5
  Keywords: agent,ai,pattern,llm
6
6
  Author: Ly Hon Quang
@@ -15,9 +15,13 @@ Classifier: Programming Language :: Python :: 3.15
15
15
  Classifier: Framework :: Pydantic :: 2
16
16
  Classifier: Operating System :: OS Independent
17
17
  Requires-Dist: a2a-sdk>=0.3.12
18
+ Requires-Dist: gymnasium>=1.2.3
19
+ Requires-Dist: nltk>=3.9.2
18
20
  Requires-Dist: pandas>=2.0.0
19
21
  Requires-Dist: rouge-score>=0.1.2
20
22
  Requires-Dist: sacrebleu>=2.5.1
23
+ Requires-Dist: scipy>=1.15.3
24
+ Requires-Dist: wandb>=0.27.0
21
25
  Requires-Python: >=3.10.12
22
26
  Project-URL: Repository, https://github.com/quanghona/agent_design_pattern.git
23
27
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aap_core"
3
- version = "0.1.3.dev1"
3
+ version = "0.2.0"
4
4
  description = "Agent design pattern core framework"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -11,9 +11,13 @@ authors = [
11
11
  requires-python = ">=3.10.12"
12
12
  dependencies = [
13
13
  "a2a-sdk>=0.3.12",
14
+ "gymnasium>=1.2.3",
15
+ "nltk>=3.9.2",
14
16
  "pandas>=2.0.0",
15
17
  "rouge-score>=0.1.2",
16
18
  "sacrebleu>=2.5.1",
19
+ "scipy>=1.15.3",
20
+ "wandb>=0.27.0",
17
21
  ]
18
22
  classifiers = [
19
23
  "Programming Language :: Python :: 3.10",
@@ -30,6 +34,10 @@ classifiers = [
30
34
  Repository = "https://github.com/quanghona/agent_design_pattern.git"
31
35
 
32
36
  [dependency-groups]
37
+ dev = [
38
+ "pytest>=9.0.3",
39
+ "ruff>=0.14.0",
40
+ ]
33
41
  lint = [
34
42
  "ruff==0.14.0",
35
43
  ]
@@ -0,0 +1,23 @@
1
+ from aap_core.policy_trainer import (
2
+ BaseExplorationModule,
3
+ BasePolicyTrainer,
4
+ DPOTrainer,
5
+ EpsilonGreedyExploration,
6
+ GRPOTrainer,
7
+ PPOTrainer,
8
+ RandomExploration,
9
+ ReinforcePPTrainer,
10
+ SimpleReplayBuffer,
11
+ )
12
+
13
+ __all__ = [
14
+ "BaseExplorationModule",
15
+ "BasePolicyTrainer",
16
+ "DPOTrainer",
17
+ "EpsilonGreedyExploration",
18
+ "GRPOTrainer",
19
+ "PPOTrainer",
20
+ "RandomExploration",
21
+ "ReinforcePPTrainer",
22
+ "SimpleReplayBuffer",
23
+ ]
@@ -5,8 +5,7 @@ from typing import Any, Dict, Literal
5
5
  from a2a.types import AgentCard
6
6
  from pydantic import BaseModel, Field, PrivateAttr
7
7
 
8
- from .chain import BaseLLMChain
9
- from .types import AgentMessage
8
+ from .types import AgentMessage, BaseLLMChain
10
9
 
11
10
 
12
11
  class BaseAgent(abc.ABC, BaseModel):
@@ -9,28 +9,7 @@ from .prompt_augmenter import (
9
9
  BasePromptAugmenter,
10
10
  IdentityPromptAugmenter,
11
11
  )
12
- from .types import AgentMessage, BaseChain, ChainMessage, ChainResponse
13
-
14
-
15
- class BaseLLMChain(BaseChain):
16
- """
17
- Base class for LLM chains.
18
- """
19
-
20
- name: str = Field(
21
- "chain",
22
- description="The name of the chain. Should be same at agent who hold this chain for easy to operate.",
23
- )
24
-
25
- @abc.abstractmethod
26
- def invoke(self, message: AgentMessage, **kwargs) -> AgentMessage:
27
- pass
28
-
29
- async def ainvoke(self, message: AgentMessage, **kwargs) -> AgentMessage:
30
- return self.invoke(message, **kwargs)
31
-
32
- def __call__(self, message: AgentMessage, **kwargs) -> AgentMessage:
33
- return self.invoke(message, **kwargs)
12
+ from .types import AgentMessage, BaseLLMChain, ChainMessage, ChainResponse, TokenUsage
34
13
 
35
14
 
36
15
  class BaseCausalMultiTurnsChain(BaseLLMChain, Generic[ChainMessage, ChainResponse]):
@@ -46,6 +25,10 @@ class BaseCausalMultiTurnsChain(BaseLLMChain, Generic[ChainMessage, ChainRespons
46
25
  default=False, description="store intermediate steps output"
47
26
  )
48
27
 
28
+ max_turns: int = Field(
29
+ default=50, description="maximum number of turns before stopping"
30
+ )
31
+
49
32
  def final_response_as_context(self, key: str) -> None:
50
33
  self._last_response_as_context = key.replace("context_", "")
51
34
 
@@ -56,7 +39,7 @@ class BaseCausalMultiTurnsChain(BaseLLMChain, Generic[ChainMessage, ChainRespons
56
39
  @abc.abstractmethod
57
40
  def _generate_response(
58
41
  self, conversation: List[ChainMessage], **kwargs
59
- ) -> Tuple[List[ChainMessage], ChainResponse, bool]:
42
+ ) -> Tuple[List[ChainMessage], ChainResponse, bool, TokenUsage]:
60
43
  pass
61
44
 
62
45
  @abc.abstractmethod
@@ -74,14 +57,38 @@ class BaseCausalMultiTurnsChain(BaseLLMChain, Generic[ChainMessage, ChainRespons
74
57
  pass
75
58
 
76
59
  def invoke(self, message: AgentMessage, **kwargs) -> AgentMessage:
77
- # Template method
60
+ if message.token_usage is None:
61
+ message.token_usage = {
62
+ "steps": [],
63
+ "total": TokenUsage(input_tokens=0, output_tokens=0, total_tokens=0),
64
+ }
65
+ if "steps" not in message.token_usage:
66
+ message.token_usage["steps"] = []
67
+ if "total" not in message.token_usage:
68
+ message.token_usage["total"] = TokenUsage(
69
+ input_tokens=0, output_tokens=0, total_tokens=0
70
+ )
71
+
72
+ def add_token(usage: TokenUsage):
73
+ message.token_usage["steps"].append(usage) # type: ignore
74
+ message.token_usage["total"]["input_tokens"] += usage["input_tokens"] # type: ignore
75
+ message.token_usage["total"]["output_tokens"] += usage["output_tokens"] # type: ignore
76
+ message.token_usage["total"]["total_tokens"] += usage["total_tokens"] # type: ignore
77
+
78
+ # Template method — ReAct loop: Thought → Act → Thought → ... → Final
78
79
  conversation = self._prepare_conversation(message)
79
- conversation, response, has_tool = self._generate_response(
80
- conversation, **kwargs
81
- )
82
- if has_tool:
80
+ turn = 0
81
+ while turn < self.max_turns:
82
+ conversation, response, has_tool, usage = self._generate_response(
83
+ conversation, **kwargs
84
+ )
85
+ add_token(usage)
86
+
87
+ if not has_tool:
88
+ break
89
+
83
90
  conversation = self._process_tools(conversation, response)
84
- conversation, _, _ = self._generate_response(conversation, **kwargs)
91
+ turn += 1
85
92
 
86
93
  message = self._append_responses(message, conversation)
87
94
 
@@ -132,13 +139,3 @@ class TypicalLLMChain(BaseLLMChain):
132
139
  message = self.generate(message, **kwargs)
133
140
  message = self.output_guardrail(message)
134
141
  return message
135
-
136
-
137
- class MetaPromptAugmenter(BasePromptAugmenter):
138
- """A prompt augmenter that use an LLM chain to rewrite the prompt."""
139
-
140
- chain: BaseLLMChain = Field(..., description="LLM chain that rewrite the prompt")
141
-
142
- def augment(self, message: AgentMessage, **kwargs) -> AgentMessage:
143
- message = self.chain.invoke(message, **kwargs)
144
- return message
@@ -10,8 +10,8 @@ from sacrebleu import sentence_bleu
10
10
 
11
11
  from aap_core.types import AgentResponse
12
12
 
13
- from .agent import AgentMessage, BaseAgent
14
- from .chain import BaseLLMChain
13
+ from .types import AgentMessage, BaseLLMChain
14
+ from .agent import BaseAgent
15
15
 
16
16
 
17
17
  class ReflectionAgent(BaseAgent):
@@ -35,6 +35,10 @@ class ReflectionAgent(BaseAgent):
35
35
  ...,
36
36
  description="LLM chain that perform reflection on the result of the main task",
37
37
  )
38
+ task_response_key: str = Field(
39
+ default="context_response",
40
+ description="The key to use for the task response in the message context.",
41
+ )
38
42
 
39
43
  @field_validator("task_response_key")
40
44
  @classmethod
@@ -245,7 +249,7 @@ class ParallelAgent(BaseAgent):
245
249
  origin=self.card.name,
246
250
  responses=[message.responses[-1] for message in responses],
247
251
  execution_result="success",
248
- ) # type: ignore
252
+ )
249
253
  self.state = "idle"
250
254
  return result_message
251
255
 
@@ -309,7 +313,7 @@ class CoordinatorAgent(BaseAgent):
309
313
  description="""The prompt that will be used by the summary chain""",
310
314
  )
311
315
  summary_steps_key: str = Field(
312
- "context_results",
316
+ default="context_results",
313
317
  description="""The key of which to store the context (e.g. intermediate result) during generation.
314
318
  Note that the key need to start with 'context_'""",
315
319
  )
@@ -330,11 +334,57 @@ class CoordinatorAgent(BaseAgent):
330
334
  return message
331
335
 
332
336
  steps = self.parse_plan(message, self.workers)
337
+
338
+ # Validate parsed plan
339
+ if not steps:
340
+ self.state = "idle"
341
+ message.execution_result = "error"
342
+ message.error_message = (
343
+ "Plan parsing failed: no steps returned from parse_plan"
344
+ )
345
+ return message
346
+
347
+ if not isinstance(steps, Sequence):
348
+ self.state = "idle"
349
+ message.execution_result = "error"
350
+ message.error_message = (
351
+ f"Plan parsing failed: expected Sequence, got {type(steps)}"
352
+ )
353
+ return message
354
+
355
+ for i, step in enumerate(steps):
356
+ if not isinstance(step, tuple) or len(step) != 3:
357
+ self.state = "idle"
358
+ message.execution_result = "error"
359
+ message.error_message = f"Plan parsing failed: step {i} is not a valid tuple of (message, agent, dependencies)"
360
+ return message
361
+ step_msg, step_agent, dependencies = step
362
+ if not isinstance(step_msg, AgentMessage):
363
+ self.state = "idle"
364
+ message.execution_result = "error"
365
+ message.error_message = (
366
+ f"Plan parsing failed: step {i} message is not an AgentMessage"
367
+ )
368
+ return message
369
+ if not isinstance(step_agent, BaseAgent):
370
+ self.state = "idle"
371
+ message.execution_result = "error"
372
+ message.error_message = (
373
+ f"Plan parsing failed: step {i} agent is not a BaseAgent"
374
+ )
375
+ return message
376
+ if not isinstance(dependencies, list):
377
+ self.state = "idle"
378
+ message.execution_result = "error"
379
+ message.error_message = (
380
+ f"Plan parsing failed: step {i} dependencies is not a list"
381
+ )
382
+ return message
383
+
333
384
  sub_task_result = []
334
385
  # TODO: check step dependency and parallelize steps to speed up
335
386
  summary_steps_key = self.summary_steps_key.replace("context_", "")
336
387
  for i, (step_msg, step_agent, dependencies) in enumerate(steps):
337
- # TODO: deal with the dependencies
338
388
  step_msg.context = {summary_steps_key: sub_task_result}
339
389
  self.state = f"step {i}: worker {step_agent.card.name} running"
340
390
  sub_task_result.append(step_agent.execute(step_msg, **kwargs))
@@ -347,7 +397,7 @@ class CoordinatorAgent(BaseAgent):
347
397
  query=self.summary_prompt,
348
398
  context={summary_steps_key: sub_task_result},
349
399
  responses=[],
350
- ) # type: ignore
400
+ )
351
401
  summary_message = self.planner_agent.execute(summary_message, **kwargs)
352
402
  else:
353
403
  return message
@@ -358,7 +408,7 @@ class CoordinatorAgent(BaseAgent):
358
408
  query=message.query,
359
409
  context={summary_steps_key: sub_task_result},
360
410
  responses=[],
361
- ) # type: ignore
411
+ )
362
412
  summary_message = self.summary_chain.invoke(summary_message, **kwargs)
363
413
 
364
414
  self.state = "idle"
@@ -521,7 +571,7 @@ class VotingAgent(BaseAgent):
521
571
  scorer = rouge_scorer.RougeScorer([self.scorer], use_stemmer=True)
522
572
 
523
573
  def rouge_score(hyp: str, ref: str) -> float:
524
- return scorer.score(ref, hyp)[self.scorer]
574
+ return list(scorer.score(ref, hyp).values())[0].fmeasure
525
575
 
526
576
  score_func = rouge_score
527
577
  else:
@@ -530,7 +580,7 @@ class VotingAgent(BaseAgent):
530
580
  scores = {agent.card.name: 0.0 for agent in self.agents}
531
581
  message_map = {name: msg for name, msg in message.responses}
532
582
  for agent_name, msg in message.responses:
533
- total_score = 0
583
+ total_score = 0.0
534
584
  for other_agent in self.agents:
535
585
  if agent_name == other_agent.card.name:
536
586
  continue
@@ -560,7 +610,7 @@ class VotingAgent(BaseAgent):
560
610
  try:
561
611
  voting_message = AgentMessage(
562
612
  query=self.voting_prompt, responses=[]
563
- ) # type: ignore
613
+ )
564
614
  voting_message = agent.execute(voting_message, **kwargs)
565
615
  total_score += self.scorer(voting_message.responses[-1][1])
566
616
  except Exception: