aap-core 0.1.2.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,7 +1,8 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: aap-core
3
- Version: 0.1.2.dev1
3
+ Version: 0.2.0
4
4
  Summary: Agent design pattern core framework
5
+ Keywords: agent,ai,pattern,llm
5
6
  Author: Ly Hon Quang
6
7
  Author-email: Ly Hon Quang <lyhonquang@gmail.com>
7
8
  License: MIT
@@ -14,10 +15,15 @@ Classifier: Programming Language :: Python :: 3.15
14
15
  Classifier: Framework :: Pydantic :: 2
15
16
  Classifier: Operating System :: OS Independent
16
17
  Requires-Dist: a2a-sdk>=0.3.12
18
+ Requires-Dist: gymnasium>=1.2.3
19
+ Requires-Dist: nltk>=3.9.2
17
20
  Requires-Dist: pandas>=2.0.0
18
21
  Requires-Dist: rouge-score>=0.1.2
19
22
  Requires-Dist: sacrebleu>=2.5.1
23
+ Requires-Dist: scipy>=1.15.3
24
+ Requires-Dist: wandb>=0.27.0
20
25
  Requires-Python: >=3.10.12
21
26
  Project-URL: Repository, https://github.com/quanghona/agent_design_pattern.git
22
27
  Description-Content-Type: text/markdown
23
28
 
29
+ # AI Agent Pattern (AAP)
@@ -0,0 +1 @@
1
+ # AI Agent Pattern (AAP)
@@ -1,18 +1,23 @@
1
1
  [project]
2
2
  name = "aap_core"
3
- version = "0.1.2.dev1"
3
+ version = "0.2.0"
4
4
  description = "Agent design pattern core framework"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
7
+ keywords = ["agent", "ai", "pattern", "llm"]
7
8
  authors = [
8
9
  { name = "Ly Hon Quang", email = "lyhonquang@gmail.com" }
9
10
  ]
10
11
  requires-python = ">=3.10.12"
11
12
  dependencies = [
12
13
  "a2a-sdk>=0.3.12",
14
+ "gymnasium>=1.2.3",
15
+ "nltk>=3.9.2",
13
16
  "pandas>=2.0.0",
14
17
  "rouge-score>=0.1.2",
15
18
  "sacrebleu>=2.5.1",
19
+ "scipy>=1.15.3",
20
+ "wandb>=0.27.0",
16
21
  ]
17
22
  classifiers = [
18
23
  "Programming Language :: Python :: 3.10",
@@ -29,6 +34,10 @@ classifiers = [
29
34
  Repository = "https://github.com/quanghona/agent_design_pattern.git"
30
35
 
31
36
  [dependency-groups]
37
+ dev = [
38
+ "pytest>=9.0.3",
39
+ "ruff>=0.14.0",
40
+ ]
32
41
  lint = [
33
42
  "ruff==0.14.0",
34
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):
@@ -5,32 +5,11 @@ from typing import Callable, Generic, List, Tuple
5
5
  from pydantic import Field, PrivateAttr
6
6
 
7
7
  from .guardrail import BaseGuardRail, PassGuardRail
8
- from .prompt_enhancer import (
9
- BasePromptEnhancer,
10
- IdentityPromptEnhancer,
8
+ from .prompt_augmenter import (
9
+ BasePromptAugmenter,
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
 
@@ -108,8 +115,8 @@ class TypicalLLMChain(BaseLLMChain):
108
115
  default=PassGuardRail(),
109
116
  description="validate the input message",
110
117
  )
111
- prompt_enhancer: BasePromptEnhancer = Field(
112
- default=IdentityPromptEnhancer(),
118
+ prompt_augmenter: BasePromptAugmenter = Field(
119
+ default=IdentityPromptAugmenter(),
113
120
  description="add more context to the prompt or rewrite / refine the prompt",
114
121
  )
115
122
  tools: Sequence[Callable] = Field(
@@ -128,21 +135,7 @@ class TypicalLLMChain(BaseLLMChain):
128
135
 
129
136
  def invoke(self, message: AgentMessage, **kwargs) -> AgentMessage:
130
137
  message = self.input_guardrail(message)
131
- message = self.prompt_enhancer(message)
138
+ message = self.prompt_augmenter(message)
132
139
  message = self.generate(message, **kwargs)
133
140
  message = self.output_guardrail(message)
134
141
  return message
135
-
136
-
137
- class LLMPromptEnhancer(BasePromptEnhancer):
138
- """A prompt enhancer that use an LLM chain to rewrite the prompt."""
139
-
140
- chain: BaseLLMChain = Field(..., description="LLM chain that rewrite the prompt")
141
-
142
- def __call__(self, message: AgentMessage, **kwargs) -> AgentMessage:
143
- message = self.chain.invoke(message, **kwargs)
144
- if message.execution_result != "success":
145
- return message
146
- _, message.query = message.responses[-1]
147
- message.responses = []
148
- 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,6 +334,53 @@ 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_", "")
@@ -346,7 +397,7 @@ class CoordinatorAgent(BaseAgent):
346
397
  query=self.summary_prompt,
347
398
  context={summary_steps_key: sub_task_result},
348
399
  responses=[],
349
- ) # type: ignore
400
+ )
350
401
  summary_message = self.planner_agent.execute(summary_message, **kwargs)
351
402
  else:
352
403
  return message
@@ -357,7 +408,7 @@ class CoordinatorAgent(BaseAgent):
357
408
  query=message.query,
358
409
  context={summary_steps_key: sub_task_result},
359
410
  responses=[],
360
- ) # type: ignore
411
+ )
361
412
  summary_message = self.summary_chain.invoke(summary_message, **kwargs)
362
413
 
363
414
  self.state = "idle"
@@ -520,7 +571,7 @@ class VotingAgent(BaseAgent):
520
571
  scorer = rouge_scorer.RougeScorer([self.scorer], use_stemmer=True)
521
572
 
522
573
  def rouge_score(hyp: str, ref: str) -> float:
523
- return scorer.score(ref, hyp)[self.scorer]
574
+ return list(scorer.score(ref, hyp).values())[0].fmeasure
524
575
 
525
576
  score_func = rouge_score
526
577
  else:
@@ -529,7 +580,7 @@ class VotingAgent(BaseAgent):
529
580
  scores = {agent.card.name: 0.0 for agent in self.agents}
530
581
  message_map = {name: msg for name, msg in message.responses}
531
582
  for agent_name, msg in message.responses:
532
- total_score = 0
583
+ total_score = 0.0
533
584
  for other_agent in self.agents:
534
585
  if agent_name == other_agent.card.name:
535
586
  continue
@@ -559,7 +610,7 @@ class VotingAgent(BaseAgent):
559
610
  try:
560
611
  voting_message = AgentMessage(
561
612
  query=self.voting_prompt, responses=[]
562
- ) # type: ignore
613
+ )
563
614
  voting_message = agent.execute(voting_message, **kwargs)
564
615
  total_score += self.scorer(voting_message.responses[-1][1])
565
616
  except Exception: