Skip to content

Commit 381935c

Browse files
authored
feat(artifact): implement Phase 2 Python Script Artifact (#526) (#547)
* feat(artifact): implement Phase 2 Python Script Artifact (#526) - Add PEP723Parser (commonMain): cross-platform PEP 723 inline script metadata parser/generator with parse, generate, injectMetadata, and stripMetadata operations. - Add PythonArtifactAgent (commonMain): SubAgent that uses LLM to generate PEP 723-compliant Python scripts. - Add PythonArtifactPackager (jvmMain): packages Python scripts with dependency resolution via uv (preferred) or pip fallback, and embeds AutoDev Unit context in the PEP 723 header. - Refactor PythonArtifactExecutor: replace 40-line local PEP 723 parsing logic with delegation to shared PEP723Parser. - Add PEP723ParserTest: 10 unit tests covering parse, generate, inject, and strip operations. * fix: address PR 547 review comments
1 parent e986bfd commit 381935c

File tree

6 files changed

+835
-36
lines changed

6 files changed

+835
-36
lines changed

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ pluginRepositoryUrl = https://github.com/unit-mesh/auto-dev
1010
pluginVersion = 2.4.6
1111

1212
# MPP Unified Version (mpp-core, mpp-ui, mpp-server)
13-
mppVersion =3.0.0-alpha5
13+
mppVersion=1.0.2
1414

1515
# Supported IDEs: idea, pycharm
1616
baseIDE=idea
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package cc.unitmesh.agent.artifact
2+
3+
/**
4+
* PEP 723 Inline Script Metadata Parser & Generator.
5+
*
6+
* Parses and generates PEP 723 compliant inline metadata blocks in Python scripts.
7+
*
8+
* PEP 723 format example:
9+
* ```python
10+
* # /// script
11+
* # requires-python = ">=3.11"
12+
* # dependencies = [
13+
* # "requests>=2.28.0",
14+
* # "pandas>=1.5.0",
15+
* # ]
16+
* # ///
17+
* ```
18+
*
19+
* @see <a href="https://peps.python.org/pep-0723/">PEP 723</a>
20+
*/
21+
object PEP723Parser {
22+
23+
/**
24+
* Parsed result from a PEP 723 metadata block.
25+
*/
26+
data class PEP723Metadata(
27+
/** Required Python version constraint, e.g. ">=3.11" */
28+
val requiresPython: String? = null,
29+
/** List of dependency specifiers, e.g. ["requests>=2.28.0", "pandas>=1.5.0"] */
30+
val dependencies: List<String> = emptyList(),
31+
/** AutoDev Unit custom metadata embedded in [tool.autodev-unit] */
32+
val autodevContext: Map<String, String> = emptyMap(),
33+
/** The raw text of the entire metadata block (including comment prefixes) */
34+
val rawBlock: String? = null
35+
)
36+
37+
// ---- Parsing ----
38+
39+
private val PEP723_BLOCK_PATTERN = Regex(
40+
"""(?s)#\s*///\s*script\s*\n(.*?)#\s*///"""
41+
)
42+
43+
private val REQUIRES_PYTHON_PATTERN = Regex(
44+
"""requires-python\s*=\s*"([^"]*)""""
45+
)
46+
47+
private val DEPENDENCIES_PATTERN = Regex(
48+
"""(?s)dependencies\s*=\s*\[(.*?)\]"""
49+
)
50+
51+
private val DEP_ITEM_PATTERN = Regex("""["']([^"']+)["']""")
52+
53+
private val AUTODEV_SECTION_PATTERN = Regex(
54+
"""(?s)\[tool\.autodev-unit\]\s*\n(.*?)(?=#\s*///|\[tool\.|$)"""
55+
)
56+
57+
private val AUTODEV_KV_PATTERN = Regex(
58+
"""#\s*(\S+)\s*=\s*"([^"]*)""""
59+
)
60+
61+
/**
62+
* Parse PEP 723 inline metadata from a Python script.
63+
*
64+
* @param pythonContent Full text of the Python script.
65+
* @return Parsed metadata, or a default empty metadata if no block is found.
66+
*/
67+
fun parse(pythonContent: String): PEP723Metadata {
68+
val blockMatch = PEP723_BLOCK_PATTERN.find(pythonContent)
69+
?: return PEP723Metadata()
70+
71+
val metadataBlock = blockMatch.groupValues[1]
72+
val rawBlock = blockMatch.value
73+
74+
// Parse requires-python
75+
val requiresPython = REQUIRES_PYTHON_PATTERN.find(metadataBlock)?.groupValues?.get(1)
76+
77+
// Parse dependencies
78+
val dependencies = parseDependencies(metadataBlock)
79+
80+
// Parse [tool.autodev-unit] section
81+
val autodevContext = parseAutodevContext(metadataBlock)
82+
83+
return PEP723Metadata(
84+
requiresPython = requiresPython,
85+
dependencies = dependencies,
86+
autodevContext = autodevContext,
87+
rawBlock = rawBlock
88+
)
89+
}
90+
91+
/**
92+
* Extract only the dependency list from a Python script (convenience method).
93+
*/
94+
fun parseDependencies(pythonContent: String): List<String> {
95+
val depsMatch = DEPENDENCIES_PATTERN.find(pythonContent) ?: return emptyList()
96+
val depsContent = depsMatch.groupValues[1]
97+
98+
return DEP_ITEM_PATTERN.findAll(depsContent)
99+
.map { it.groupValues[1] }
100+
.toList()
101+
}
102+
103+
private fun parseAutodevContext(metadataBlock: String): Map<String, String> {
104+
val sectionMatch = AUTODEV_SECTION_PATTERN.find(metadataBlock)
105+
?: return emptyMap()
106+
107+
val sectionContent = sectionMatch.groupValues[1]
108+
return AUTODEV_KV_PATTERN.findAll(sectionContent)
109+
.associate { it.groupValues[1] to it.groupValues[2] }
110+
}
111+
112+
// ---- Generation ----
113+
114+
/**
115+
* Generate a PEP 723 metadata header block.
116+
*
117+
* @param dependencies List of dependency specifiers (e.g. "requests>=2.28.0").
118+
* @param requiresPython Python version constraint (default ">=3.11").
119+
* @param autodevContext Optional AutoDev Unit context key-value pairs to embed.
120+
* @return The generated metadata block as a string, ready to prepend to a script.
121+
*/
122+
fun generate(
123+
dependencies: List<String> = emptyList(),
124+
requiresPython: String = ">=3.11",
125+
autodevContext: Map<String, String> = emptyMap()
126+
): String = buildString {
127+
appendLine("# /// script")
128+
appendLine("# requires-python = \"$requiresPython\"")
129+
130+
if (dependencies.isNotEmpty()) {
131+
appendLine("# dependencies = [")
132+
dependencies.forEach { dep ->
133+
appendLine("# \"$dep\",")
134+
}
135+
appendLine("# ]")
136+
}
137+
138+
if (autodevContext.isNotEmpty()) {
139+
appendLine("# [tool.autodev-unit]")
140+
autodevContext.forEach { (key, value) ->
141+
appendLine("# $key = \"$value\"")
142+
}
143+
}
144+
145+
appendLine("# ///")
146+
}
147+
148+
/**
149+
* Inject or replace a PEP 723 metadata block in a Python script.
150+
*
151+
* If the script already contains a PEP 723 block, it is replaced.
152+
* Otherwise the new block is prepended.
153+
*
154+
* @param pythonContent The original script content.
155+
* @param dependencies Dependency list.
156+
* @param requiresPython Python version constraint.
157+
* @param autodevContext Optional AutoDev context map.
158+
* @return The script with the metadata block injected/replaced.
159+
*/
160+
fun injectMetadata(
161+
pythonContent: String,
162+
dependencies: List<String> = emptyList(),
163+
requiresPython: String = ">=3.11",
164+
autodevContext: Map<String, String> = emptyMap()
165+
): String {
166+
val newBlock = generate(dependencies, requiresPython, autodevContext)
167+
168+
return if (PEP723_BLOCK_PATTERN.containsMatchIn(pythonContent)) {
169+
PEP723_BLOCK_PATTERN.replace(pythonContent, newBlock.trimEnd())
170+
} else {
171+
newBlock + "\n" + pythonContent
172+
}
173+
}
174+
175+
/**
176+
* Strip the PEP 723 metadata block from a Python script, returning only the code body.
177+
*/
178+
fun stripMetadata(pythonContent: String): String {
179+
return PEP723_BLOCK_PATTERN.replace(pythonContent, "").trimStart('\r', '\n')
180+
}
181+
}

0 commit comments

Comments
 (0)