Plan step
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
from enum import Enum
|
||||
from typing import Literal
|
||||
|
||||
|
||||
class Language(str, Enum):
|
||||
PYTHON = "python"
|
||||
JAVASCRIPT = "javascript"
|
||||
TYPESCRIPT = "typescript"
|
||||
JAVA = "java"
|
||||
GO = "go"
|
||||
RUST = "rust"
|
||||
CPP = "cpp"
|
||||
C = "c"
|
||||
|
||||
LanguageLiteral = Literal[
|
||||
Language.PYTHON,
|
||||
Language.JAVASCRIPT,
|
||||
Language.TYPESCRIPT,
|
||||
Language.JAVA,
|
||||
Language.GO,
|
||||
Language.RUST,
|
||||
Language.CPP,
|
||||
Language.C,
|
||||
]
|
||||
@@ -0,0 +1,51 @@
|
||||
import pandas as pd
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
|
||||
from contracts.RepoContext import Language
|
||||
|
||||
|
||||
class DependencyNode(BaseModel):
|
||||
"""A single node in the dependency graph."""
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
id: str = Field(..., description="Unique identifier (file path or module name)")
|
||||
label: str = Field(..., description="Human-readable label")
|
||||
language: Language
|
||||
is_external: bool = Field(
|
||||
default=False,
|
||||
description="True if the dependency is outside the repository",
|
||||
)
|
||||
|
||||
|
||||
class DependencyEdge(BaseModel):
|
||||
"""A directed edge from source → target (source depends on target)."""
|
||||
source: str = Field(..., description="Node id of the dependant")
|
||||
target: str = Field(..., description="Node id of the dependency")
|
||||
kind: str = Field(
|
||||
default="import",
|
||||
description="Edge kind: import, include, inherit, call, etc.",
|
||||
)
|
||||
|
||||
|
||||
class DependencyGraph(BaseModel):
|
||||
"""Complete dependency graph result."""
|
||||
root: str = Field(..., description="Entrypoint node id")
|
||||
nodes: list[DependencyNode]
|
||||
edges: list[DependencyEdge]
|
||||
metadata: dict[str, str] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class AnalyzeResult(BaseModel):
|
||||
"""Return shape of :func:`analyze`."""
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
df: pd.DataFrame
|
||||
isolated: list[DependencyNode]
|
||||
leaf_no_deps: list[DependencyNode]
|
||||
entry_no_dependents: list[DependencyNode]
|
||||
widely_used: list[tuple[DependencyNode, int]]
|
||||
used_by_packages: dict[DependencyNode, int]
|
||||
wccs: list[set[DependencyNode]]
|
||||
sccs: list[set[DependencyNode]]
|
||||
node2comm: dict[DependencyNode, int]
|
||||
comms: list[set[DependencyNode]]
|
||||
+38
-75
@@ -1,65 +1,40 @@
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Optional, Literal, Any
|
||||
from typing import Optional, Literal, Any, Generic
|
||||
|
||||
from pydantic import field_validator, BaseModel, Field
|
||||
import pandas as pd
|
||||
from pydantic import field_validator, BaseModel, Field, ConfigDict, field_serializer
|
||||
|
||||
from contracts.CodeLanguages import Language
|
||||
from contracts.GraphAnalyzer import AnalyzeResult, DependencyGraph
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class RepoContext:
|
||||
url: str
|
||||
default_branch: str
|
||||
language_hint: str | None # "python" | "typescript" | "go" | None
|
||||
|
||||
class Language(str, Enum):
|
||||
PYTHON = "python"
|
||||
JAVASCRIPT = "javascript"
|
||||
TYPESCRIPT = "typescript"
|
||||
JAVA = "java"
|
||||
GO = "go"
|
||||
RUST = "rust"
|
||||
CPP = "cpp"
|
||||
C = "c"
|
||||
|
||||
LanguageLiteral = Literal[
|
||||
Language.PYTHON,
|
||||
Language.JAVASCRIPT,
|
||||
Language.TYPESCRIPT,
|
||||
Language.JAVA,
|
||||
Language.GO,
|
||||
Language.RUST,
|
||||
Language.CPP,
|
||||
Language.C,
|
||||
]
|
||||
|
||||
class DependencyNode(BaseModel):
|
||||
"""A single node in the dependency graph."""
|
||||
id: str = Field(..., description="Unique identifier (file path or module name)")
|
||||
label: str = Field(..., description="Human-readable label")
|
||||
language: Language
|
||||
is_external: bool = Field(
|
||||
default=False,
|
||||
description="True if the dependency is outside the repository",
|
||||
)
|
||||
"""Result object carrying all info a coding agent needs."""
|
||||
remote_url: str
|
||||
branch: str
|
||||
base_branch: str
|
||||
head_commit: str
|
||||
is_new_clone: bool
|
||||
is_new_branch: bool
|
||||
repo_dir: Path = Field(..., description="Absolute path to the cloned repository")
|
||||
original_branch: Optional[str] = None
|
||||
uncommitted_changes: Optional[str] = None # git diff (tracked)
|
||||
untracked_files: list[str] = field(default_factory=list)
|
||||
stash_ref: Optional[str] = None
|
||||
env: dict = field(default_factory=dict)
|
||||
|
||||
|
||||
class DependencyEdge(BaseModel):
|
||||
"""A directed edge from source → target (source depends on target)."""
|
||||
source: str = Field(..., description="Node id of the dependant")
|
||||
target: str = Field(..., description="Node id of the dependency")
|
||||
kind: str = Field(
|
||||
default="import",
|
||||
description="Edge kind: import, include, inherit, call, etc.",
|
||||
)
|
||||
@field_validator("repo_path")
|
||||
@classmethod
|
||||
def repo_must_exist(cls, v: Path) -> Path:
|
||||
if not v.is_dir():
|
||||
raise ValueError(f"Repository path does not exist or is not a directory: {v}")
|
||||
return v.resolve()
|
||||
|
||||
|
||||
class DependencyGraph(BaseModel):
|
||||
"""Complete dependency graph result."""
|
||||
root: str = Field(..., description="Entrypoint node id")
|
||||
nodes: list[DependencyNode]
|
||||
edges: list[DependencyEdge]
|
||||
metadata: dict[str, str] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ToolRequirement(BaseModel):
|
||||
@@ -81,33 +56,21 @@ class AnalysisError(BaseModel):
|
||||
|
||||
class AnalysisInput(BaseModel):
|
||||
"""Input schema for the analyzer."""
|
||||
repo_path: Path = Field(..., description="Absolute path to the cloned repository")
|
||||
language: Language
|
||||
entrypoint: str = Field(
|
||||
...,
|
||||
description="Relative path from repo_path to the entrypoint file",
|
||||
)
|
||||
|
||||
@field_validator("repo_path")
|
||||
@classmethod
|
||||
def repo_must_exist(cls, v: Path) -> Path:
|
||||
if not v.is_dir():
|
||||
raise ValueError(f"Repository path does not exist or is not a directory: {v}")
|
||||
return v.resolve()
|
||||
|
||||
@field_validator("entrypoint")
|
||||
@classmethod
|
||||
def entrypoint_must_exist(cls, v: str, info) -> str:
|
||||
repo = info.data.get("repo_path")
|
||||
if repo and not (repo / v).is_file():
|
||||
raise ValueError(f"Entrypoint not found: {repo / v}")
|
||||
return v
|
||||
repo: RepoContext
|
||||
|
||||
|
||||
class AnalysisResult(BaseModel):
|
||||
"""Top-level result — either a graph or an error."""
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
success: bool
|
||||
graph: Optional[DependencyGraph] = None
|
||||
error: Optional[AnalysisError] = None
|
||||
leaf_clusters: Any
|
||||
res_all: Any
|
||||
leaf_clusters: Any = None
|
||||
res_all: AnalyzeResult | None = None
|
||||
|
||||
@field_serializer("leaf_clusters")
|
||||
def _ser_leaf_clusters(self, v: Any, _info):
|
||||
if isinstance(v, pd.DataFrame):
|
||||
return v.to_dict(orient="records")
|
||||
return v # already JSON-safe
|
||||
@@ -0,0 +1,16 @@
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from contracts.base import _StrictModel
|
||||
|
||||
|
||||
class FileSelection(_StrictModel):
|
||||
path: str = Field(...)
|
||||
role: Literal["primary", "context", "test"] = Field(...)
|
||||
reason: str = Field(...)
|
||||
confidence: float = Field(ge=0.0, le=1.0)
|
||||
|
||||
class FileSelectionOutput(_StrictModel):
|
||||
selections: list[FileSelection] = Field(...)
|
||||
notes: list[str] = Field(...)
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
import secrets
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from agents.coders.state import AgentState
|
||||
@@ -15,9 +16,13 @@ from common.youtrack_mcp_client import YouTrackMCPClient
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AgentConfig:
|
||||
agent_dir: Path
|
||||
|
||||
# ── project-level ──────────────────────────────────────────
|
||||
youtrack_project: str
|
||||
gitea_repo: str
|
||||
git_name: str
|
||||
git_email: str
|
||||
gitea_repo: str = ""
|
||||
gitea_target_branch: str = "main"
|
||||
|
||||
# ── limits (snapshotted into state at run start) ───────────
|
||||
@@ -39,6 +44,13 @@ class AgentConfig:
|
||||
# ── language hints (for Plan) ──────────────────────────────
|
||||
language_markers: dict[str, list[str]] = None # filled from project
|
||||
|
||||
def project_mem_dir(self):
|
||||
p = self.agent_dir / "memory" / f"{self.youtrack_project}"
|
||||
if not p.exists():
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
return p
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class RunContext:
|
||||
|
||||
Reference in New Issue
Block a user