awslabs.cdk-mcp-server 0.1.1__py3-none-any.whl → 0.1.3__py3-none-any.whl
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.
- awslabs/cdk_mcp_server/core/resources.py +104 -15
- awslabs/cdk_mcp_server/core/server.py +4 -3
- awslabs/cdk_mcp_server/core/tools.py +6 -1
- awslabs/cdk_mcp_server/data/genai_cdk_loader.py +508 -349
- {awslabs_cdk_mcp_server-0.1.1.dist-info → awslabs_cdk_mcp_server-0.1.3.dist-info}/METADATA +24 -1
- awslabs_cdk_mcp_server-0.1.3.dist-info/RECORD +33 -0
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/actiongroups.md +0 -137
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/alias.md +0 -39
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/collaboration.md +0 -91
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/creation.md +0 -149
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/custom_orchestration.md +0 -74
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/overview.md +0 -78
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/prompt_override.md +0 -70
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/bedrockguardrails.md +0 -188
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/chunking.md +0 -137
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/datasources.md +0 -225
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/kendra.md +0 -81
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/overview.md +0 -116
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/parsing.md +0 -36
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/transformation.md +0 -30
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/vector/aurora.md +0 -185
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/vector/creation.md +0 -80
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/vector/opensearch.md +0 -56
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/vector/pinecone.md +0 -66
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/profiles.md +0 -153
- awslabs/cdk_mcp_server/static/genai_cdk/opensearch-vectorindex/overview.md +0 -135
- awslabs/cdk_mcp_server/static/genai_cdk/opensearchserverless/overview.md +0 -17
- awslabs_cdk_mcp_server-0.1.1.dist-info/RECORD +0 -54
- {awslabs_cdk_mcp_server-0.1.1.dist-info → awslabs_cdk_mcp_server-0.1.3.dist-info}/WHEEL +0 -0
- {awslabs_cdk_mcp_server-0.1.1.dist-info → awslabs_cdk_mcp_server-0.1.3.dist-info}/entry_points.txt +0 -0
- {awslabs_cdk_mcp_server-0.1.1.dist-info → awslabs_cdk_mcp_server-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {awslabs_cdk_mcp_server-0.1.1.dist-info → awslabs_cdk_mcp_server-0.1.3.dist-info}/licenses/NOTICE +0 -0
|
@@ -9,429 +9,588 @@
|
|
|
9
9
|
# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
|
|
10
10
|
# and limitations under the License.
|
|
11
11
|
|
|
12
|
-
"""GenAI CDK constructs
|
|
12
|
+
"""GitHub-based GenAI CDK constructs content loader."""
|
|
13
13
|
|
|
14
|
+
import httpx
|
|
14
15
|
import logging
|
|
15
|
-
import
|
|
16
|
-
from
|
|
17
|
-
from enum import Enum
|
|
16
|
+
import re
|
|
17
|
+
from datetime import datetime, timedelta
|
|
18
18
|
from typing import Any, Dict, List, Optional
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
# Set up logging
|
|
22
22
|
logger = logging.getLogger(__name__)
|
|
23
23
|
|
|
24
|
+
# Constants
|
|
25
|
+
GITHUB_API_URL = 'https://api.github.com'
|
|
26
|
+
GITHUB_RAW_CONTENT_URL = 'https://raw.githubusercontent.com'
|
|
27
|
+
REPO_OWNER = 'awslabs'
|
|
28
|
+
REPO_NAME = 'generative-ai-cdk-constructs'
|
|
29
|
+
BASE_PATH = 'src/cdk-lib'
|
|
30
|
+
CACHE_TTL = timedelta(hours=24) # Cache for 24 hours
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
# Simple caches
|
|
33
|
+
_readme_cache = {} # Cache for README.md content, keyed by path
|
|
34
|
+
_sections_cache = {} # Cache for extracted sections, keyed by path
|
|
35
|
+
_constructs_cache = {} # Cache for constructs list
|
|
36
|
+
_last_constructs_fetch = None # Last time constructs were fetched
|
|
27
37
|
|
|
28
|
-
BEDROCK = 'bedrock'
|
|
29
|
-
OPENSEARCH_SERVERLESS = 'opensearchserverless'
|
|
30
|
-
OPENSEARCH_VECTOR_INDEX = 'opensearch-vectorindex'
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def get_construct_map() -> Dict[str, str]:
|
|
39
|
-
"""Get a dictionary mapping construct types to their descriptions."""
|
|
40
|
-
return {
|
|
41
|
-
'bedrock': 'Amazon Bedrock constructs for agents, knowledge bases, and more',
|
|
42
|
-
'opensearchserverless': 'Amazon OpenSearch Serverless constructs for vector search',
|
|
43
|
-
'opensearch-vectorindex': 'Amazon OpenSearch vector index constructs',
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def get_genai_cdk_overview(construct_type: str = '') -> str:
|
|
48
|
-
"""Get an overview of GenAI CDK constructs.
|
|
39
|
+
async def fetch_readme(
|
|
40
|
+
construct_type: str, construct_name: Optional[str] = None
|
|
41
|
+
) -> Dict[str, Any]:
|
|
42
|
+
"""Fetch README.md content directly from GitHub.
|
|
49
43
|
|
|
50
44
|
Args:
|
|
51
|
-
construct_type:
|
|
52
|
-
|
|
45
|
+
construct_type: Top-level directory (e.g., 'bedrock')
|
|
46
|
+
construct_name: Optional subdirectory (e.g., 'agents')
|
|
53
47
|
|
|
54
48
|
Returns:
|
|
55
|
-
|
|
49
|
+
Dictionary with README content and metadata
|
|
56
50
|
"""
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
'
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
'
|
|
51
|
+
# Build the path
|
|
52
|
+
path_parts = [construct_type]
|
|
53
|
+
if construct_name:
|
|
54
|
+
path_parts.append(construct_name)
|
|
55
|
+
|
|
56
|
+
path = '/'.join(path_parts)
|
|
57
|
+
cache_key = f'{construct_type}/{construct_name}' if construct_name else construct_type
|
|
58
|
+
|
|
59
|
+
# Check cache first
|
|
60
|
+
if (
|
|
61
|
+
cache_key in _readme_cache
|
|
62
|
+
and datetime.now() - _readme_cache[cache_key]['timestamp'] < CACHE_TTL
|
|
63
|
+
):
|
|
64
|
+
logger.debug(f'Using cached README for {path}')
|
|
65
|
+
return _readme_cache[cache_key]['data']
|
|
66
|
+
|
|
67
|
+
# Fetch from GitHub
|
|
68
|
+
readme_url = (
|
|
69
|
+
f'{GITHUB_RAW_CONTENT_URL}/{REPO_OWNER}/{REPO_NAME}/main/{BASE_PATH}/{path}/README.md'
|
|
72
70
|
)
|
|
71
|
+
logger.info(f'Fetching README from {readme_url}')
|
|
72
|
+
|
|
73
73
|
try:
|
|
74
|
-
with
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
async with httpx.AsyncClient() as client:
|
|
75
|
+
response = await client.get(readme_url)
|
|
76
|
+
|
|
77
|
+
if response.status_code != 200:
|
|
78
|
+
logger.warning(f'Failed to fetch README for {path}: HTTP {response.status_code}')
|
|
79
|
+
return {
|
|
80
|
+
'error': f'Failed to fetch README for {path}: HTTP {response.status_code}',
|
|
81
|
+
'status_code': response.status_code,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
content = response.text
|
|
85
|
+
|
|
86
|
+
# Update cache
|
|
87
|
+
result = {
|
|
88
|
+
'content': content,
|
|
89
|
+
'path': path,
|
|
90
|
+
'url': readme_url,
|
|
91
|
+
'status': 'success',
|
|
92
|
+
}
|
|
78
93
|
|
|
94
|
+
_readme_cache[cache_key] = {
|
|
95
|
+
'timestamp': datetime.now(),
|
|
96
|
+
'data': result,
|
|
97
|
+
}
|
|
79
98
|
|
|
80
|
-
|
|
81
|
-
|
|
99
|
+
return result
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.error(f'Error fetching README for {path}: {str(e)}')
|
|
102
|
+
return {
|
|
103
|
+
'error': f'Error fetching README: {str(e)}',
|
|
104
|
+
'status': 'error',
|
|
105
|
+
}
|
|
82
106
|
|
|
83
|
-
Args:
|
|
84
|
-
construct_type: The construct type (e.g., 'bedrock')
|
|
85
|
-
construct_name: The name of the construct (e.g., 'agent', 'knowledgebases')
|
|
86
107
|
|
|
87
|
-
|
|
88
|
-
|
|
108
|
+
def extract_sections(content: str) -> Dict[str, str]:
|
|
109
|
+
"""Extract sections from README.md content based on level 2 headings (##) only.
|
|
110
|
+
|
|
111
|
+
Returns a dictionary mapping section names to their content.
|
|
112
|
+
Uses URL encoding for section names to handle special characters.
|
|
89
113
|
"""
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
os.path.dirname(os.path.dirname(__file__)), # Fix path to use parent directory
|
|
93
|
-
'static',
|
|
94
|
-
'genai_cdk',
|
|
95
|
-
construct_type,
|
|
96
|
-
construct_name,
|
|
97
|
-
)
|
|
114
|
+
# Find all level 2 headings (## Heading)
|
|
115
|
+
headings = re.finditer(r'^##\s+(.+?)$', content, re.MULTILINE)
|
|
98
116
|
|
|
99
|
-
|
|
100
|
-
|
|
117
|
+
sections = {}
|
|
118
|
+
section_starts = []
|
|
101
119
|
|
|
102
|
-
#
|
|
103
|
-
for root, dirs, files in os.walk(base_path):
|
|
104
|
-
rel_path = os.path.relpath(root, base_path)
|
|
120
|
+
# Import here to avoid circular imports
|
|
105
121
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
122
|
+
# Collect all level 2 headings with their positions
|
|
123
|
+
for match in headings:
|
|
124
|
+
heading_text = match.group(1).strip()
|
|
125
|
+
# Store URL-safe version for proper matching later
|
|
126
|
+
section_starts.append((match.start(), heading_text))
|
|
109
127
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
128
|
+
# Sort by position
|
|
129
|
+
section_starts.sort()
|
|
130
|
+
|
|
131
|
+
# Extract content between headings
|
|
132
|
+
for i, (start_pos, heading) in enumerate(section_starts):
|
|
133
|
+
# Find the end of this section (start of next level 2 heading or end of file)
|
|
134
|
+
end_pos = section_starts[i + 1][0] if i < len(section_starts) - 1 else len(content)
|
|
135
|
+
|
|
136
|
+
# Extract the section content including the heading
|
|
137
|
+
section_content = content[start_pos:end_pos].strip()
|
|
138
|
+
|
|
139
|
+
# Use the heading text as the key
|
|
140
|
+
sections[heading] = section_content
|
|
120
141
|
|
|
121
142
|
return sections
|
|
122
143
|
|
|
123
144
|
|
|
124
|
-
def
|
|
125
|
-
|
|
145
|
+
async def get_section(
|
|
146
|
+
construct_type: str, construct_name: str, section_name: str
|
|
147
|
+
) -> Dict[str, Any]:
|
|
148
|
+
"""Get a specific section from a README.md file.
|
|
126
149
|
|
|
127
150
|
Args:
|
|
128
|
-
construct_type:
|
|
129
|
-
construct_name:
|
|
130
|
-
|
|
151
|
+
construct_type: Top-level directory (e.g., 'bedrock')
|
|
152
|
+
construct_name: Subdirectory (e.g., 'agents')
|
|
153
|
+
section_name: Name of the section to extract
|
|
131
154
|
|
|
132
155
|
Returns:
|
|
133
|
-
|
|
156
|
+
Dictionary with section content and metadata
|
|
134
157
|
"""
|
|
135
|
-
#
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
#
|
|
140
|
-
if
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
construct_type,
|
|
182
|
-
construct_name_lower,
|
|
183
|
-
f'{section}.md',
|
|
184
|
-
)
|
|
158
|
+
# Build cache key
|
|
159
|
+
path = f'{construct_type}/{construct_name}'
|
|
160
|
+
cache_key = path
|
|
161
|
+
|
|
162
|
+
# Check if sections are already cached
|
|
163
|
+
if (
|
|
164
|
+
cache_key in _sections_cache
|
|
165
|
+
and datetime.now() - _sections_cache[cache_key]['timestamp'] < CACHE_TTL
|
|
166
|
+
):
|
|
167
|
+
sections = _sections_cache[cache_key]['data']
|
|
168
|
+
|
|
169
|
+
# Find the section (case-insensitive)
|
|
170
|
+
for heading, content in sections.items():
|
|
171
|
+
if heading.lower() == section_name.lower():
|
|
172
|
+
return {
|
|
173
|
+
'content': content,
|
|
174
|
+
'section': heading,
|
|
175
|
+
'path': path,
|
|
176
|
+
'status': 'success',
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# Section not found in cache
|
|
180
|
+
return {
|
|
181
|
+
'error': f"Section '{section_name}' not found in {path}",
|
|
182
|
+
'status': 'not_found',
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# Fetch the README
|
|
186
|
+
readme_result = await fetch_readme(construct_type, construct_name)
|
|
187
|
+
|
|
188
|
+
if 'error' in readme_result:
|
|
189
|
+
# Return error result with consistent path
|
|
190
|
+
return {
|
|
191
|
+
'error': readme_result['error'],
|
|
192
|
+
'path': path,
|
|
193
|
+
'status': 'error',
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# Extract sections
|
|
197
|
+
sections = extract_sections(readme_result['content'])
|
|
198
|
+
|
|
199
|
+
# Cache the sections
|
|
200
|
+
_sections_cache[cache_key] = {
|
|
201
|
+
'timestamp': datetime.now(),
|
|
202
|
+
'data': sections,
|
|
203
|
+
}
|
|
185
204
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
205
|
+
# Find the section using URL decoding and case-insensitive comparison
|
|
206
|
+
import urllib.parse
|
|
207
|
+
|
|
208
|
+
decoded_section_name = urllib.parse.unquote(section_name)
|
|
209
|
+
logger.info(f"Looking for section '{decoded_section_name}' in {path}")
|
|
210
|
+
logger.info(f'Available sections: {", ".join(sections.keys())}')
|
|
211
|
+
|
|
212
|
+
# First try direct match after decoding
|
|
213
|
+
for heading, content in sections.items():
|
|
214
|
+
if heading.lower() == decoded_section_name.lower():
|
|
215
|
+
return {
|
|
216
|
+
'content': content,
|
|
217
|
+
'section': heading,
|
|
218
|
+
'path': path,
|
|
219
|
+
'status': 'success',
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# Section not found
|
|
223
|
+
logger.warning(f"Section '{section_name}' not found in {path}")
|
|
224
|
+
return {
|
|
225
|
+
'error': f"Section '{section_name}' not found in {path}",
|
|
226
|
+
'status': 'not_found',
|
|
227
|
+
}
|
|
193
228
|
|
|
194
229
|
|
|
195
|
-
def
|
|
196
|
-
"""
|
|
230
|
+
async def list_sections(construct_type: str, construct_name: str) -> Dict[str, Any]:
|
|
231
|
+
"""List available sections in a README.md file.
|
|
197
232
|
|
|
198
233
|
Args:
|
|
199
|
-
construct_type:
|
|
200
|
-
construct_name:
|
|
234
|
+
construct_type: Top-level directory (e.g., 'bedrock')
|
|
235
|
+
construct_name: Subdirectory (e.g., 'agents')
|
|
201
236
|
|
|
202
237
|
Returns:
|
|
203
|
-
|
|
238
|
+
Dictionary with list of sections and metadata
|
|
204
239
|
"""
|
|
205
|
-
#
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
#
|
|
210
|
-
if
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
# Check if directory exists
|
|
241
|
-
if not os.path.exists(base_path):
|
|
242
|
-
return f"Error: Documentation for '{construct_name}' in '{construct_type}' not found."
|
|
243
|
-
|
|
244
|
-
# List files in directory
|
|
245
|
-
sections = []
|
|
246
|
-
for file_name in os.listdir(base_path):
|
|
247
|
-
if file_name.endswith('.md') and file_name != 'overview.md':
|
|
248
|
-
sections.append(file_name[:-3]) # Remove .md extension
|
|
249
|
-
|
|
250
|
-
# Also check subdirectories
|
|
251
|
-
for root, dirs, files in os.walk(base_path):
|
|
252
|
-
if root != base_path: # Skip the base directory
|
|
253
|
-
rel_path = os.path.relpath(root, base_path)
|
|
254
|
-
for file_name in files:
|
|
255
|
-
if file_name.endswith('.md'):
|
|
256
|
-
section_path = os.path.join(rel_path, file_name[:-3])
|
|
257
|
-
section_path = section_path.replace('\\', '/')
|
|
258
|
-
sections.append(section_path)
|
|
259
|
-
|
|
260
|
-
result = f'# {construct_name.capitalize()} Documentation\n\n'
|
|
261
|
-
result += 'This documentation is split into sections for easier consumption.\n\n'
|
|
262
|
-
result += '## Available Sections\n\n'
|
|
263
|
-
|
|
264
|
-
for section in sorted(sections):
|
|
265
|
-
result += f'- [{section}](genai-cdk-constructs://{construct_type}/{construct_name_lower}/{section})\n'
|
|
266
|
-
|
|
267
|
-
return result
|
|
268
|
-
|
|
269
|
-
# Special handling for key constructs
|
|
270
|
-
key_construct_mapping = {
|
|
271
|
-
'agent': 'agent',
|
|
272
|
-
'agents': 'agent',
|
|
273
|
-
'knowledgebase': 'knowledgebases',
|
|
274
|
-
'knowledgebases': 'knowledgebases',
|
|
275
|
-
'knowledge-base': 'knowledgebases',
|
|
276
|
-
'knowledge-bases': 'knowledgebases',
|
|
277
|
-
'agentactiongroup': 'agent/actiongroups',
|
|
278
|
-
'action-group': 'agent/actiongroups',
|
|
279
|
-
'actiongroup': 'agent/actiongroups',
|
|
280
|
-
'agentalias': 'agent/alias',
|
|
281
|
-
'guardrail': 'bedrockguardrails',
|
|
282
|
-
'guardrails': 'bedrockguardrails',
|
|
283
|
-
'bedrock-guardrails': 'bedrockguardrails',
|
|
240
|
+
# Build cache key
|
|
241
|
+
path = f'{construct_type}/{construct_name}'
|
|
242
|
+
cache_key = path
|
|
243
|
+
|
|
244
|
+
# Check if sections are already cached
|
|
245
|
+
if (
|
|
246
|
+
cache_key in _sections_cache
|
|
247
|
+
and datetime.now() - _sections_cache[cache_key]['timestamp'] < CACHE_TTL
|
|
248
|
+
):
|
|
249
|
+
sections = _sections_cache[cache_key]['data']
|
|
250
|
+
return {
|
|
251
|
+
'sections': list(sections.keys()),
|
|
252
|
+
'path': path,
|
|
253
|
+
'status': 'success',
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# Fetch the README
|
|
257
|
+
readme_result = await fetch_readme(construct_type, construct_name)
|
|
258
|
+
|
|
259
|
+
if 'error' in readme_result:
|
|
260
|
+
# Return empty sections on error, but maintain successful status
|
|
261
|
+
return {
|
|
262
|
+
'sections': [],
|
|
263
|
+
'path': path,
|
|
264
|
+
'status': 'success',
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
# Extract sections
|
|
268
|
+
sections = extract_sections(readme_result['content'])
|
|
269
|
+
|
|
270
|
+
# Cache the sections
|
|
271
|
+
_sections_cache[cache_key] = {
|
|
272
|
+
'timestamp': datetime.now(),
|
|
273
|
+
'data': sections,
|
|
284
274
|
}
|
|
285
275
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
parent, section = mapped_name.split('/', 1)
|
|
292
|
-
return get_genai_cdk_construct_section(construct_type, parent, section)
|
|
293
|
-
else:
|
|
294
|
-
construct_name_lower = mapped_name
|
|
295
|
-
|
|
296
|
-
# Validate construct type
|
|
297
|
-
if construct_type not in get_construct_types():
|
|
298
|
-
construct_list = '\n'.join([f'- {t}: {desc}' for t, desc in get_construct_map().items()])
|
|
299
|
-
return f"# GenAI CDK Constructs\n\nConstruct type '{construct_type}' not found. Available types:\n\n{construct_list}"
|
|
300
|
-
|
|
301
|
-
# Get construct file (flat structure)
|
|
302
|
-
file_path = os.path.join(
|
|
303
|
-
os.path.dirname(os.path.dirname(__file__)), # Fix path to use parent directory
|
|
304
|
-
'static',
|
|
305
|
-
'genai_cdk',
|
|
306
|
-
construct_type,
|
|
307
|
-
f'{construct_name_lower}.md',
|
|
308
|
-
)
|
|
309
|
-
try:
|
|
310
|
-
with open(file_path, 'r', encoding='utf-8') as f:
|
|
311
|
-
return f.read()
|
|
312
|
-
except FileNotFoundError:
|
|
313
|
-
# Try to see if this is a directory with an overview.md file
|
|
314
|
-
overview_path = os.path.join(
|
|
315
|
-
os.path.dirname(os.path.dirname(__file__)),
|
|
316
|
-
'static',
|
|
317
|
-
'genai_cdk',
|
|
318
|
-
construct_type,
|
|
319
|
-
construct_name_lower,
|
|
320
|
-
'overview.md',
|
|
321
|
-
)
|
|
322
|
-
try:
|
|
323
|
-
with open(overview_path, 'r', encoding='utf-8') as f:
|
|
324
|
-
return f.read()
|
|
325
|
-
except FileNotFoundError:
|
|
326
|
-
return f"Error: Documentation for '{construct_name}' in '{construct_type}' not found."
|
|
276
|
+
return {
|
|
277
|
+
'sections': list(sections.keys()),
|
|
278
|
+
'path': path,
|
|
279
|
+
'status': 'success',
|
|
280
|
+
}
|
|
327
281
|
|
|
328
282
|
|
|
329
|
-
def
|
|
330
|
-
"""
|
|
283
|
+
async def get_construct_overview(construct_type: str) -> Dict[str, Any]:
|
|
284
|
+
"""Get overview documentation for a construct type.
|
|
331
285
|
|
|
332
286
|
Args:
|
|
333
|
-
construct_type:
|
|
287
|
+
construct_type: Top-level directory (e.g., 'bedrock')
|
|
334
288
|
|
|
335
289
|
Returns:
|
|
336
|
-
|
|
290
|
+
Dictionary with README content for the construct type
|
|
337
291
|
"""
|
|
338
|
-
|
|
292
|
+
return await fetch_readme(construct_type)
|
|
339
293
|
|
|
340
|
-
# Determine which construct types to search
|
|
341
|
-
if construct_type is not None:
|
|
342
|
-
construct_types = [construct_type.lower()]
|
|
343
|
-
else:
|
|
344
|
-
construct_types = get_construct_types()
|
|
345
294
|
|
|
346
|
-
|
|
347
|
-
for
|
|
348
|
-
if ct not in get_construct_types():
|
|
349
|
-
continue
|
|
295
|
+
async def fetch_bedrock_subdirectories() -> List[Dict[str, Any]]:
|
|
296
|
+
"""Fetch subdirectories specifically for the bedrock directory.
|
|
350
297
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
298
|
+
Returns:
|
|
299
|
+
List of subdirectory information
|
|
300
|
+
"""
|
|
301
|
+
try:
|
|
302
|
+
async with httpx.AsyncClient() as client:
|
|
303
|
+
response = await client.get(
|
|
304
|
+
f'{GITHUB_API_URL}/repos/{REPO_OWNER}/{REPO_NAME}/contents/{BASE_PATH}/bedrock',
|
|
305
|
+
headers={'Accept': 'application/vnd.github.v3+json'},
|
|
306
|
+
)
|
|
355
307
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
308
|
+
if response.status_code != 200:
|
|
309
|
+
logger.warning(
|
|
310
|
+
f'Failed to fetch bedrock subdirectories: HTTP {response.status_code}'
|
|
311
|
+
)
|
|
312
|
+
return []
|
|
313
|
+
|
|
314
|
+
contents = response.json()
|
|
315
|
+
|
|
316
|
+
# Filter directories only
|
|
317
|
+
subdirs = []
|
|
318
|
+
for item in contents:
|
|
319
|
+
if item['type'] == 'dir':
|
|
320
|
+
subdir_name = item['name']
|
|
321
|
+
|
|
322
|
+
# Get README for this subdirectory if available
|
|
323
|
+
readme_result = await fetch_readme('bedrock', subdir_name)
|
|
324
|
+
|
|
325
|
+
# Default values
|
|
326
|
+
title = subdir_name
|
|
327
|
+
description = f'Bedrock {subdir_name.capitalize()} constructs'
|
|
328
|
+
|
|
329
|
+
# Extract better title/description if README exists
|
|
330
|
+
if 'error' not in readme_result:
|
|
331
|
+
readme_content = readme_result['content']
|
|
332
|
+
|
|
333
|
+
# Use a safer approach to extract title - find first # heading
|
|
334
|
+
lines = readme_content.split('\n')
|
|
335
|
+
for line in lines:
|
|
336
|
+
if line.startswith('# '):
|
|
337
|
+
title = line.replace('# ', '').strip()
|
|
338
|
+
break
|
|
339
|
+
|
|
340
|
+
# Extract description from content after first heading and before next heading
|
|
341
|
+
# or stability banner
|
|
342
|
+
description_text = ''
|
|
343
|
+
capture_description = False
|
|
344
|
+
for line in lines:
|
|
345
|
+
if line.startswith('# '):
|
|
346
|
+
capture_description = True
|
|
347
|
+
continue
|
|
348
|
+
if capture_description and (
|
|
349
|
+
line.startswith('#') or line.startswith('<!--BEGIN')
|
|
350
|
+
):
|
|
351
|
+
break
|
|
352
|
+
if capture_description and line.strip():
|
|
353
|
+
description_text += line.strip() + ' '
|
|
354
|
+
|
|
355
|
+
if description_text:
|
|
356
|
+
# Clean up and truncate description
|
|
357
|
+
description_text = description_text.strip()
|
|
358
|
+
# Take first sentence or up to 150 chars
|
|
359
|
+
description = description_text.split('.')[0][:150]
|
|
360
|
+
if len(description) < len(description_text):
|
|
361
|
+
description += '...'
|
|
362
|
+
|
|
363
|
+
subdirs.append(
|
|
364
|
+
{
|
|
365
|
+
'name': title,
|
|
366
|
+
'path': f'bedrock/{subdir_name}',
|
|
367
|
+
'url': item['html_url'],
|
|
368
|
+
'description': description,
|
|
369
|
+
}
|
|
370
|
+
)
|
|
359
371
|
|
|
360
|
-
|
|
361
|
-
|
|
372
|
+
return subdirs
|
|
373
|
+
except Exception as e:
|
|
374
|
+
logger.error(f'Error fetching bedrock subdirectories: {str(e)}')
|
|
375
|
+
return []
|
|
362
376
|
|
|
363
|
-
# Process subdirectories recursively
|
|
364
|
-
for root, dirs, files in os.walk(dir_path):
|
|
365
|
-
# Skip the main directory as it's already processed
|
|
366
|
-
if root == dir_path:
|
|
367
|
-
continue
|
|
368
377
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
# Use the relative path as the parent
|
|
372
|
-
process_directory_files(root, ct, constructs, parent=rel_path.replace(os.sep, '_'))
|
|
378
|
+
async def fetch_repo_structure() -> Dict[str, Any]:
|
|
379
|
+
"""Fetch repository structure from GitHub API.
|
|
373
380
|
|
|
374
|
-
|
|
381
|
+
Returns:
|
|
382
|
+
Dictionary with repository structure information
|
|
383
|
+
"""
|
|
384
|
+
global _constructs_cache, _last_constructs_fetch
|
|
375
385
|
|
|
386
|
+
# Check if we've fetched recently
|
|
387
|
+
if _last_constructs_fetch and datetime.now() - _last_constructs_fetch < CACHE_TTL:
|
|
388
|
+
logger.debug('Using cached repo structure')
|
|
389
|
+
return _constructs_cache
|
|
390
|
+
|
|
391
|
+
try:
|
|
392
|
+
# Fetch top-level directories
|
|
393
|
+
async with httpx.AsyncClient() as client:
|
|
394
|
+
response = await client.get(
|
|
395
|
+
f'{GITHUB_API_URL}/repos/{REPO_OWNER}/{REPO_NAME}/contents/{BASE_PATH}',
|
|
396
|
+
headers={'Accept': 'application/vnd.github.v3+json'},
|
|
397
|
+
)
|
|
376
398
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
399
|
+
if response.status_code != 200:
|
|
400
|
+
logger.warning(f'Failed to fetch repo structure: HTTP {response.status_code}')
|
|
401
|
+
return {'error': 'Failed to fetch repository structure'}
|
|
402
|
+
|
|
403
|
+
contents = response.json()
|
|
404
|
+
|
|
405
|
+
# Filter directories only
|
|
406
|
+
directories = [item for item in contents if item['type'] == 'dir']
|
|
407
|
+
|
|
408
|
+
# For each directory, get its README.md if available
|
|
409
|
+
construct_types = {}
|
|
410
|
+
for dir_info in directories:
|
|
411
|
+
dir_name = dir_info['name']
|
|
412
|
+
|
|
413
|
+
# Initialize default values first
|
|
414
|
+
title = dir_name
|
|
415
|
+
description = f'AWS {dir_name.capitalize()} constructs'
|
|
416
|
+
|
|
417
|
+
# Then fetch and potentially override with better data
|
|
418
|
+
readme_result = await fetch_readme(dir_name)
|
|
419
|
+
if 'error' not in readme_result:
|
|
420
|
+
# Try to extract title and description from README content using markdown parsing
|
|
421
|
+
readme_content = readme_result['content']
|
|
422
|
+
|
|
423
|
+
# Use a safer approach to extract title - find first # heading
|
|
424
|
+
lines = readme_content.split('\n')
|
|
425
|
+
for line in lines:
|
|
426
|
+
if line.startswith('# '):
|
|
427
|
+
title = line.replace('# ', '').strip()
|
|
428
|
+
break
|
|
429
|
+
|
|
430
|
+
# Extract description from content after first heading and before next heading
|
|
431
|
+
# or stability banner
|
|
432
|
+
description_text = ''
|
|
433
|
+
capture_description = False
|
|
434
|
+
for line in lines:
|
|
435
|
+
if line.startswith('# '):
|
|
436
|
+
capture_description = True
|
|
437
|
+
continue
|
|
438
|
+
if capture_description and (
|
|
439
|
+
line.startswith('#') or line.startswith('<!--BEGIN')
|
|
440
|
+
):
|
|
441
|
+
break
|
|
442
|
+
if capture_description and line.strip():
|
|
443
|
+
description_text += line.strip() + ' '
|
|
444
|
+
|
|
445
|
+
if description_text:
|
|
446
|
+
# Clean up and truncate description
|
|
447
|
+
description_text = description_text.strip()
|
|
448
|
+
# Take first sentence or up to 150 chars
|
|
449
|
+
description = description_text.split('.')[0][:150]
|
|
450
|
+
if len(description) < len(description_text):
|
|
451
|
+
description += '...'
|
|
452
|
+
|
|
453
|
+
# Store in construct types
|
|
454
|
+
construct_types[dir_name] = {
|
|
455
|
+
'name': title,
|
|
456
|
+
'description': description,
|
|
457
|
+
'path': dir_info['path'],
|
|
458
|
+
'url': dir_info['html_url'],
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
# Special case for bedrock: fetch its subdirectories
|
|
462
|
+
if 'bedrock' in construct_types:
|
|
463
|
+
bedrock_subdirs = await fetch_bedrock_subdirectories()
|
|
464
|
+
if bedrock_subdirs:
|
|
465
|
+
construct_types['bedrock']['subdirectories'] = bedrock_subdirs
|
|
466
|
+
|
|
467
|
+
# Update cache
|
|
468
|
+
_constructs_cache = {'construct_types': construct_types}
|
|
469
|
+
_last_constructs_fetch = datetime.now()
|
|
470
|
+
|
|
471
|
+
return _constructs_cache
|
|
472
|
+
except Exception as e:
|
|
473
|
+
logger.error(f'Error fetching repo structure: {str(e)}')
|
|
474
|
+
return {'error': f'Error fetching repository structure: {str(e)}'}
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
async def list_available_constructs(construct_type: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
478
|
+
"""List available constructs from GitHub.
|
|
384
479
|
|
|
385
480
|
Args:
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
481
|
+
construct_type: Optional construct type to filter by
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
List of constructs with name, type, and description
|
|
390
485
|
"""
|
|
391
|
-
#
|
|
392
|
-
|
|
393
|
-
# Skip overview file, directories, and non-markdown files
|
|
394
|
-
if (
|
|
395
|
-
file_name == 'overview.md'
|
|
396
|
-
or not file_name.endswith('.md')
|
|
397
|
-
or os.path.isdir(os.path.join(dir_path, file_name))
|
|
398
|
-
):
|
|
399
|
-
continue
|
|
486
|
+
# Get repository structure
|
|
487
|
+
repo_structure = await fetch_repo_structure()
|
|
400
488
|
|
|
401
|
-
|
|
402
|
-
|
|
489
|
+
if 'error' in repo_structure:
|
|
490
|
+
logger.error(f'Error in list_available_constructs: {repo_structure["error"]}')
|
|
491
|
+
return []
|
|
403
492
|
|
|
404
|
-
|
|
405
|
-
if parent:
|
|
406
|
-
construct_name = f'{parent}_{base_name}'
|
|
407
|
-
else:
|
|
408
|
-
construct_name = base_name
|
|
493
|
+
construct_types = repo_structure.get('construct_types', {})
|
|
409
494
|
|
|
410
|
-
|
|
495
|
+
# Get available types
|
|
496
|
+
available_types = list(construct_types.keys())
|
|
411
497
|
|
|
412
|
-
|
|
413
|
-
|
|
498
|
+
# If construct type is provided, filter by it
|
|
499
|
+
if construct_type:
|
|
500
|
+
if construct_type not in available_types:
|
|
501
|
+
logger.warning(
|
|
502
|
+
f"Construct type '{construct_type}' not found. Available types: {', '.join(available_types)}"
|
|
503
|
+
)
|
|
504
|
+
return []
|
|
505
|
+
filter_types = [construct_type]
|
|
506
|
+
else:
|
|
507
|
+
filter_types = available_types
|
|
414
508
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
description = descriptions.get(display_name, '')
|
|
509
|
+
# Prepare result list
|
|
510
|
+
constructs = []
|
|
418
511
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
first_line = f.readline().strip()
|
|
424
|
-
description = (
|
|
425
|
-
first_line[1:].strip() if first_line.startswith('#') else display_name
|
|
426
|
-
)
|
|
427
|
-
except Exception:
|
|
428
|
-
description = f'A {construct_type} construct.'
|
|
512
|
+
# For each construct type
|
|
513
|
+
for ct in filter_types:
|
|
514
|
+
# Get README for this construct type
|
|
515
|
+
readme_result = await fetch_readme(ct)
|
|
429
516
|
|
|
430
|
-
|
|
517
|
+
if 'error' in readme_result:
|
|
518
|
+
continue
|
|
519
|
+
|
|
520
|
+
# Extract sections from README
|
|
521
|
+
sections = extract_sections(readme_result['content'])
|
|
522
|
+
|
|
523
|
+
# Add construct types as top-level constructs
|
|
431
524
|
constructs.append(
|
|
432
525
|
{
|
|
433
|
-
'name':
|
|
434
|
-
'type':
|
|
435
|
-
'description': description,
|
|
526
|
+
'name': ct.capitalize(),
|
|
527
|
+
'type': ct,
|
|
528
|
+
'description': construct_types[ct]['description'],
|
|
436
529
|
}
|
|
437
530
|
)
|
|
531
|
+
|
|
532
|
+
# Add sections as constructs
|
|
533
|
+
for section_name in sections:
|
|
534
|
+
# Build a construct name from section
|
|
535
|
+
name_parts = [part.capitalize() for part in section_name.split()]
|
|
536
|
+
if len(name_parts) > 1:
|
|
537
|
+
construct_name = f'{name_parts[0]}{"".join(name_parts[1:])}'
|
|
538
|
+
else:
|
|
539
|
+
construct_name = name_parts[0]
|
|
540
|
+
|
|
541
|
+
# Build description from the first line of the section
|
|
542
|
+
section_content = sections[section_name]
|
|
543
|
+
first_line = section_content.split('\n')[0].strip('# ')
|
|
544
|
+
description = first_line
|
|
545
|
+
|
|
546
|
+
# Add to constructs list
|
|
547
|
+
constructs.append(
|
|
548
|
+
{
|
|
549
|
+
'name': construct_name,
|
|
550
|
+
'type': ct,
|
|
551
|
+
'description': description,
|
|
552
|
+
}
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
# Add bedrock subdirectories as constructs
|
|
556
|
+
if ct == 'bedrock' and 'subdirectories' in construct_types[ct]:
|
|
557
|
+
for subdir in construct_types[ct]['subdirectories']:
|
|
558
|
+
# Add the subdirectory as a construct
|
|
559
|
+
subdir_name = subdir['name']
|
|
560
|
+
constructs.append(
|
|
561
|
+
{
|
|
562
|
+
'name': f'{subdir_name}',
|
|
563
|
+
'type': 'bedrock',
|
|
564
|
+
'description': subdir['description'],
|
|
565
|
+
}
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
# Also fetch README for this subdirectory to extract sections
|
|
569
|
+
subdir_raw_name = subdir['path'].split('/')[-1] # Get the raw name from path
|
|
570
|
+
subdir_readme = await fetch_readme('bedrock', subdir_raw_name)
|
|
571
|
+
if 'error' not in subdir_readme:
|
|
572
|
+
subdir_sections = extract_sections(subdir_readme['content'])
|
|
573
|
+
|
|
574
|
+
# Add sections from subdirectory README
|
|
575
|
+
for section_name in subdir_sections:
|
|
576
|
+
# Similar logic to build construct name and description
|
|
577
|
+
name_parts = [part.capitalize() for part in section_name.split()]
|
|
578
|
+
if len(name_parts) > 1:
|
|
579
|
+
section_construct_name = f'{name_parts[0]}{"".join(name_parts[1:])}'
|
|
580
|
+
else:
|
|
581
|
+
section_construct_name = name_parts[0]
|
|
582
|
+
|
|
583
|
+
section_content = subdir_sections[section_name]
|
|
584
|
+
first_line = section_content.split('\n')[0].strip('# ')
|
|
585
|
+
description = first_line
|
|
586
|
+
|
|
587
|
+
# Add to constructs list with special naming to indicate subdirectory
|
|
588
|
+
constructs.append(
|
|
589
|
+
{
|
|
590
|
+
'name': f'{subdir_name}{section_construct_name}',
|
|
591
|
+
'type': 'bedrock',
|
|
592
|
+
'description': description,
|
|
593
|
+
}
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
return constructs
|