github-agent 0.2.12__tar.gz → 0.2.14__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.4
2
2
  Name: github-agent
3
- Version: 0.2.12
3
+ Version: 0.2.14
4
4
  Summary: GitHub Agent for MCP
5
5
  Author-email: Audel Rouhi <knucklessg1@gmail.com>
6
6
  License: MIT
@@ -12,7 +12,8 @@ Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: >=3.10
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
- Requires-Dist: pydantic-ai-slim[a2a,ag-ui,anthropic,fastmcp,google,groq,huggingface,mistral,openai,web]>=1.58.0
15
+ Requires-Dist: tree-sitter>=0.23.2
16
+ Requires-Dist: pydantic-ai-slim[a2a,ag-ui,anthropic,fastmcp,google,groq,huggingface,mistral,openai,web]>=1.60.0
16
17
  Requires-Dist: pydantic-ai-skills>=v0.4.0
17
18
  Requires-Dist: fastapi>=0.128.0
18
19
  Requires-Dist: fastmcp
@@ -43,7 +44,7 @@ Dynamic: license-file
43
44
  ![PyPI - Wheel](https://img.shields.io/pypi/wheel/github-agent)
44
45
  ![PyPI - Implementation](https://img.shields.io/pypi/implementation/github-agent)
45
46
 
46
- *Version: 0.2.12*
47
+ *Version: 0.2.14*
47
48
 
48
49
  ## Overview
49
50
 
@@ -21,7 +21,7 @@
21
21
  ![PyPI - Wheel](https://img.shields.io/pypi/wheel/github-agent)
22
22
  ![PyPI - Implementation](https://img.shields.io/pypi/implementation/github-agent)
23
23
 
24
- *Version: 0.2.12*
24
+ *Version: 0.2.14*
25
25
 
26
26
  ## Overview
27
27
 
@@ -39,7 +39,7 @@ from pydantic import ValidationError
39
39
  from pydantic_ai.ui import SSE_CONTENT_TYPE
40
40
  from pydantic_ai.ui.ag_ui import AGUIAdapter
41
41
 
42
- __version__ = "0.2.12"
42
+ __version__ = "0.2.14"
43
43
 
44
44
  logging.basicConfig(
45
45
  level=logging.INFO,
@@ -363,7 +363,23 @@ def create_agent(
363
363
  ),
364
364
  }
365
365
 
366
+ # 1. Identify Universal Skills
367
+ # Universal skills are those in the skills directory that do NOT start with the package prefix
368
+ package_prefix = "github-"
369
+ skills_path = get_skills_path()
370
+ universal_skill_dirs = []
371
+
372
+ if os.path.exists(skills_path):
373
+ for item in os.listdir(skills_path):
374
+ item_path = os.path.join(skills_path, item)
375
+ if os.path.isdir(item_path):
376
+ if not item.startswith(package_prefix):
377
+ universal_skill_dirs.append(item_path)
378
+ logger.info(f"Identified universal skill: {item}")
379
+
380
+ supervisor_skills = []
366
381
  child_agents = {}
382
+ supervisor_skills_directories = [get_skills_path()]
367
383
 
368
384
  for tag, (system_prompt, agent_name) in agent_defs.items():
369
385
  tag_toolsets = []
@@ -381,21 +397,28 @@ def create_agent(
381
397
  # Load specific skills for this tag
382
398
  skill_dir_name = f"github-{tag.replace('_', '-')}"
383
399
 
400
+ child_skills_directories = []
401
+
384
402
  # Check custom skills directory
385
403
  if custom_skills_directory:
386
404
  skill_dir_path = os.path.join(custom_skills_directory, skill_dir_name)
387
405
  if os.path.exists(skill_dir_path):
388
- tag_toolsets.append(SkillsToolset(directories=[skill_dir_path]))
389
- logger.info(
390
- f"Loaded specialized skills for {tag} from {skill_dir_path}"
391
- )
406
+ child_skills_directories.append(skill_dir_path)
392
407
 
393
408
  # Check default skills directory
394
409
  default_skill_path = os.path.join(get_skills_path(), skill_dir_name)
395
410
  if os.path.exists(default_skill_path):
396
- tag_toolsets.append(SkillsToolset(directories=[default_skill_path]))
411
+ child_skills_directories.append(default_skill_path)
412
+
413
+ # Append Universal Skills to ALL child agents
414
+ if universal_skill_dirs:
415
+ child_skills_directories.extend(universal_skill_dirs)
416
+
417
+ if child_skills_directories:
418
+ ts = SkillsToolset(directories=child_skills_directories)
419
+ tag_toolsets.append(ts)
397
420
  logger.info(
398
- f"Loaded specialized skills for {tag} from {default_skill_path}"
421
+ f"Loaded specialized skills for {tag} from {child_skills_directories}"
399
422
  )
400
423
 
401
424
  # Collect tool names for logging
@@ -434,11 +457,43 @@ def create_agent(
434
457
  )
435
458
  child_agents[tag] = agent
436
459
 
460
+ # Create Custom Agent if custom_skills_directory is provided
461
+ if custom_skills_directory:
462
+ custom_agent_tag = "custom_agent"
463
+ custom_agent_name = "Custom_Agent"
464
+ custom_agent_prompt = (
465
+ "You are the Custom Agent.\n"
466
+ "Your goal is to handle custom tasks or general tasks not covered by other specialists.\n"
467
+ "You have access to valid custom skills and universal skills."
468
+ )
469
+
470
+ custom_agent_skills_dirs = list(universal_skill_dirs)
471
+ custom_agent_skills_dirs.append(custom_skills_directory)
472
+
473
+ custom_toolsets = []
474
+ custom_toolsets.append(SkillsToolset(directories=custom_agent_skills_dirs))
475
+
476
+ custom_agent = Agent(
477
+ name=custom_agent_name,
478
+ system_prompt=custom_agent_prompt,
479
+ model=model,
480
+ model_settings=settings,
481
+ toolsets=custom_toolsets,
482
+ tool_timeout=DEFAULT_TOOL_TIMEOUT,
483
+ )
484
+ child_agents[custom_agent_tag] = custom_agent
485
+
486
+ if custom_skills_directory:
487
+ supervisor_skills_directories.append(custom_skills_directory)
488
+ supervisor_skills.append(SkillsToolset(directories=supervisor_skills_directories))
489
+ logger.info(f"Loaded supervisor skills from: {supervisor_skills_directories}")
490
+
437
491
  supervisor = Agent(
438
492
  name=AGENT_NAME,
439
493
  system_prompt=SUPERVISOR_SYSTEM_PROMPT,
440
494
  model=model,
441
495
  model_settings=settings,
496
+ toolsets=supervisor_skills,
442
497
  deps_type=Any,
443
498
  )
444
499
 
@@ -707,6 +762,17 @@ def create_agent(
707
762
  logger.exception(f"Error in Support Docs Agent: {e}")
708
763
  return f"Error executing task for Support Docs Agent: {e}"
709
764
 
765
+ if "custom_agent" in child_agents:
766
+
767
+ @supervisor.tool
768
+ async def assign_task_to_custom_agent(ctx: RunContext[Any], task: str) -> str:
769
+ """Assign a task to the Custom Agent."""
770
+ return (
771
+ await child_agents["custom_agent"]
772
+ .run(task, usage=ctx.usage, deps=ctx.deps)
773
+ .output
774
+ )
775
+
710
776
  return supervisor
711
777
 
712
778
 
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env python3
2
+ import asyncio
3
+ import httpx
4
+ import json
5
+ import uuid
6
+ import argparse
7
+ import sys
8
+
9
+
10
+ async def validate_agent_card(client, agent_url):
11
+ """
12
+ Validates the agent by fetching its well-known agent card.
13
+ """
14
+ card_url = f"{agent_url.rstrip('/')}/.well-known/agent-card.json"
15
+ print(f"Fetching agent card from: {card_url}")
16
+ try:
17
+ resp = await client.get(card_url)
18
+ if resp.status_code == 200:
19
+ try:
20
+ card_data = resp.json()
21
+ print(f"Agent Card Found: {json.dumps(card_data, indent=2)}")
22
+ return True
23
+ except json.JSONDecodeError:
24
+ print(f"Failed to decode agent card JSON from {card_url}")
25
+ return False
26
+ else:
27
+ print(f"Failed to fetch agent card. Status Code: {resp.status_code}")
28
+ return False
29
+ except httpx.RequestError as e:
30
+ print(f"Connection failed to {card_url}: {e}")
31
+ return False
32
+
33
+
34
+ async def send_message(client, agent_url, message_text):
35
+ """
36
+ Sends a message to the agent via JSON-RPC.
37
+ """
38
+ print(f"\nSending Message: '{message_text}' to {agent_url}")
39
+
40
+ payload = {
41
+ "jsonrpc": "2.0",
42
+ "method": "message/send",
43
+ "params": {
44
+ "message": {
45
+ "kind": "message",
46
+ "role": "user",
47
+ "parts": [{"kind": "text", "text": message_text}],
48
+ "messageId": str(uuid.uuid4()),
49
+ }
50
+ },
51
+ "id": 1,
52
+ }
53
+
54
+ try:
55
+ resp = await client.post(
56
+ agent_url, json=payload, headers={"Content-Type": "application/json"}
57
+ )
58
+
59
+ if resp.status_code != 200:
60
+ print(f"Error sending message. Status Code: {resp.status_code}")
61
+ print(resp.text)
62
+ return None
63
+
64
+ data = resp.json()
65
+ if "error" in data:
66
+ print(f"JSON-RPC Error: {data['error']}")
67
+ return None
68
+
69
+ if "result" in data and "id" in data["result"]:
70
+ task_id = data["result"]["id"]
71
+ print(f"Task Submitted with ID: {task_id}")
72
+ return task_id
73
+ else:
74
+ print(f"Unexpected response format: {data}")
75
+ return None
76
+
77
+ except httpx.RequestError as e:
78
+ print(f"Connection failed during message send: {e}")
79
+ return None
80
+ except json.JSONDecodeError:
81
+ print(f"Failed to decode response JSON: {resp.text}")
82
+ return None
83
+
84
+
85
+ async def poll_task(client, agent_url, task_id):
86
+ """
87
+ Polls the task status until completion.
88
+ """
89
+ print(f"Polling for result for Task ID: {task_id}...")
90
+
91
+ while True:
92
+ await asyncio.sleep(2)
93
+ poll_payload = {
94
+ "jsonrpc": "2.0",
95
+ "method": "tasks/get",
96
+ "params": {"id": task_id},
97
+ "id": 2,
98
+ }
99
+
100
+ try:
101
+ poll_resp = await client.post(
102
+ agent_url,
103
+ json=poll_payload,
104
+ headers={"Content-Type": "application/json"},
105
+ )
106
+
107
+ if poll_resp.status_code != 200:
108
+ print(f"Polling Failed: {poll_resp.status_code}")
109
+ print(f"Details: {poll_resp.text}")
110
+ break
111
+
112
+ poll_data = poll_resp.json()
113
+
114
+ if "error" in poll_data:
115
+ print(f"Polling Error: {poll_data['error']}")
116
+ break
117
+
118
+ if "result" in poll_data:
119
+ status = poll_data["result"].get("status", {})
120
+ state = status.get("state")
121
+ print(f"Task State: {state}")
122
+
123
+ if state not in ["submitted", "running", "working"]:
124
+ print(f"\nTask Finished with state: {state}")
125
+ return poll_data["result"]
126
+ else:
127
+ print(f"Unexpected polling response: {poll_data}")
128
+ break
129
+
130
+ except httpx.RequestError as e:
131
+ print(f"Connection failed during polling: {e}")
132
+ break
133
+ except json.JSONDecodeError:
134
+ print(f"Failed to decode polling response: {poll_resp.text}")
135
+ break
136
+
137
+
138
+ def print_result(result):
139
+ """
140
+ Prints the final result from the agent.
141
+ """
142
+ if not result:
143
+ return
144
+
145
+ history = result.get("history", [])
146
+ if history:
147
+ last_msg = None
148
+ # Find the last message that is NOT from the user (i.e., the agent's response)
149
+ for msg in reversed(history):
150
+ if msg.get("role") != "user":
151
+ last_msg = msg
152
+ break
153
+
154
+ if last_msg:
155
+ print("\n--- Agent Response ---")
156
+ if "parts" in last_msg:
157
+ for part in last_msg["parts"]:
158
+ if "text" in part:
159
+ print(part["text"])
160
+ elif "content" in part:
161
+ print(part["content"])
162
+ else:
163
+ print(f"Final Message (No parts): {last_msg}")
164
+ else:
165
+ print("\n--- No Agent Response Found in History ---")
166
+
167
+ # print(f"\nFull Result Debug:\n{json.dumps(result, indent=2)}")
168
+
169
+
170
+ async def main():
171
+ parser = argparse.ArgumentParser(
172
+ description="A2A Client for communicating with other agents."
173
+ )
174
+ parser.add_argument(
175
+ "--url",
176
+ required=True,
177
+ help="The base URL of the A2A Agent (e.g., http://agent.arpa/a2a/)",
178
+ )
179
+ parser.add_argument(
180
+ "--query", required=True, help="The message/query to send to the agent"
181
+ )
182
+
183
+ args = parser.parse_args()
184
+
185
+ agent_url = args.url
186
+ query = args.query
187
+
188
+ print("Initializing A2A Client...")
189
+ print(f"Target Agent: {agent_url}")
190
+ print(f"Query: {query}")
191
+
192
+ async with httpx.AsyncClient(timeout=60.0) as client:
193
+ # 1. Validate Agent
194
+ if not await validate_agent_card(client, agent_url):
195
+ print("Agent validation failed. Aborting.")
196
+ sys.exit(1)
197
+
198
+ # 2. Send Message
199
+ task_id = await send_message(client, agent_url, query)
200
+ if not task_id:
201
+ print("Failed to submit task. Aborting.")
202
+ sys.exit(1)
203
+
204
+ # 3. Poll for Result
205
+ result = await poll_task(client, agent_url, task_id)
206
+
207
+ # 4. Print Result
208
+ print_result(result)
209
+
210
+
211
+ if __name__ == "__main__":
212
+ asyncio.run(main())
@@ -2,9 +2,22 @@
2
2
  # coding: utf-8
3
3
 
4
4
  import os
5
+ import httpx
6
+ import pickle
7
+ import yaml
8
+ import logging
9
+ from pathlib import Path
10
+ from typing import Union, List, Any, Optional
11
+ import json
12
+ from importlib.resources import files, as_file
13
+ from pydantic_ai.models.openai import OpenAIChatModel
14
+ from pydantic_ai.models.google import GoogleModel
15
+ from pydantic_ai.models.huggingface import HuggingFaceModel
16
+ from pydantic_ai.models.groq import GroqModel
17
+ from pydantic_ai.models.mistral import MistralModel
18
+ from fasta2a import Skill
5
19
 
6
20
  try:
7
-
8
21
  from openai import AsyncOpenAI
9
22
  from pydantic_ai.providers.openai import OpenAIProvider
10
23
  except ImportError:
@@ -26,26 +39,16 @@ except ImportError:
26
39
  MistralProvider = None
27
40
 
28
41
  try:
42
+ from pydantic_ai.models.anthropic import AnthropicModel
29
43
  from anthropic import AsyncAnthropic
30
44
  from pydantic_ai.providers.anthropic import AnthropicProvider
31
45
  except ImportError:
46
+ AnthropicModel = None
32
47
  AsyncAnthropic = None
33
48
  AnthropicProvider = None
34
49
 
35
- import httpx
36
- import pickle
37
- import yaml
38
- from pathlib import Path
39
- from typing import Any, Union, List, Optional
40
- import json
41
- from importlib.resources import files, as_file
42
- from pydantic_ai.models.openai import OpenAIChatModel
43
- from pydantic_ai.models.anthropic import AnthropicModel
44
- from pydantic_ai.models.google import GoogleModel
45
- from pydantic_ai.models.huggingface import HuggingFaceModel
46
- from pydantic_ai.models.groq import GroqModel
47
- from pydantic_ai.models.mistral import MistralModel
48
- from pydantic_ai_skills import Skill
50
+
51
+ logger = logging.getLogger(__name__)
49
52
 
50
53
 
51
54
  def to_integer(string: Union[str, int] = None) -> int:
@@ -163,6 +166,9 @@ def load_model(file: str) -> Any:
163
166
  def retrieve_package_name() -> str:
164
167
  """
165
168
  Returns the top-level package name of the module that imported this utils.py.
169
+
170
+ Works reliably when utils.py is inside a proper package (with __init__.py or
171
+ implicit namespace package) and the caller does normal imports.
166
172
  """
167
173
  if __package__:
168
174
  top = __package__.partition(".")[0]
@@ -363,6 +369,16 @@ def create_model(
363
369
  def extract_tool_tags(tool_def: Any) -> List[str]:
364
370
  """
365
371
  Extracts tags from a tool definition object.
372
+
373
+ Found structure in debug:
374
+ tool_def.name (str)
375
+ tool_def.meta (dict) -> {'fastmcp': {'tags': ['tag']}}
376
+
377
+ This function checks multiple paths to be robust:
378
+ 1. tool_def.meta['fastmcp']['tags']
379
+ 2. tool_def.meta['tags']
380
+ 3. tool_def.metadata['tags'] (legacy/alternative wrapper)
381
+ 4. tool_def.metadata.get('meta')... (nested path)
366
382
  """
367
383
  tags_list = []
368
384
 
@@ -409,3 +425,10 @@ def tool_in_tag(tool_def: Any, tag: str) -> bool:
409
425
  return True
410
426
  else:
411
427
  return False
428
+
429
+
430
+ def filter_tools_by_tag(tools: List[Any], tag: str) -> List[Any]:
431
+ """
432
+ Filters a list of tools for a given tag.
433
+ """
434
+ return [t for t in tools if tool_in_tag(t, tag)]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-agent
3
- Version: 0.2.12
3
+ Version: 0.2.14
4
4
  Summary: GitHub Agent for MCP
5
5
  Author-email: Audel Rouhi <knucklessg1@gmail.com>
6
6
  License: MIT
@@ -12,7 +12,8 @@ Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: >=3.10
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
- Requires-Dist: pydantic-ai-slim[a2a,ag-ui,anthropic,fastmcp,google,groq,huggingface,mistral,openai,web]>=1.58.0
15
+ Requires-Dist: tree-sitter>=0.23.2
16
+ Requires-Dist: pydantic-ai-slim[a2a,ag-ui,anthropic,fastmcp,google,groq,huggingface,mistral,openai,web]>=1.60.0
16
17
  Requires-Dist: pydantic-ai-skills>=v0.4.0
17
18
  Requires-Dist: fastapi>=0.128.0
18
19
  Requires-Dist: fastmcp
@@ -43,7 +44,7 @@ Dynamic: license-file
43
44
  ![PyPI - Wheel](https://img.shields.io/pypi/wheel/github-agent)
44
45
  ![PyPI - Implementation](https://img.shields.io/pypi/implementation/github-agent)
45
46
 
46
- *Version: 0.2.12*
47
+ *Version: 0.2.14*
47
48
 
48
49
  ## Overview
49
50
 
@@ -10,4 +10,5 @@ github_agent.egg-info/dependency_links.txt
10
10
  github_agent.egg-info/entry_points.txt
11
11
  github_agent.egg-info/requires.txt
12
12
  github_agent.egg-info/top_level.txt
13
+ github_agent/skills/a2a_client/scripts/a2a_client.py
13
14
  scripts/validate_a2a_agent.py
@@ -1,4 +1,5 @@
1
- pydantic-ai-slim[a2a,ag-ui,anthropic,fastmcp,google,groq,huggingface,mistral,openai,web]>=1.58.0
1
+ tree-sitter>=0.23.2
2
+ pydantic-ai-slim[a2a,ag-ui,anthropic,fastmcp,google,groq,huggingface,mistral,openai,web]>=1.60.0
2
3
  pydantic-ai-skills>=v0.4.0
3
4
  fastapi>=0.128.0
4
5
  fastmcp
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "github-agent"
7
- version = "0.2.12"
7
+ version = "0.2.14"
8
8
  readme = "README.md"
9
9
  description = "GitHub Agent for MCP"
10
10
  requires-python = ">=3.10"
@@ -18,7 +18,8 @@ classifiers = [
18
18
  "Programming Language :: Python :: 3",
19
19
  ]
20
20
  dependencies = [
21
- "pydantic-ai-slim[fastmcp,openai,anthropic,groq,mistral,google,huggingface,a2a,ag-ui,web]>=1.58.0",
21
+ "tree-sitter>=0.23.2",
22
+ "pydantic-ai-slim[fastmcp,openai,groq,anthropic,mistral,google,huggingface,a2a,ag-ui,web]>=1.60.0",
22
23
  "pydantic-ai-skills>=v0.4.0",
23
24
  "fastapi>=0.128.0",
24
25
  "fastmcp",
File without changes
File without changes