struai 0.0.1__py3-none-any.whl → 0.1.0__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.
@@ -0,0 +1,60 @@
1
+ """StruAI models."""
2
+ from .common import BBox, Dimensions, Point, TextSpan
3
+ from .drawings import (
4
+ Annotations,
5
+ DetailTag,
6
+ DrawingResult,
7
+ Leader,
8
+ RevisionCloud,
9
+ RevisionTriangle,
10
+ SectionTag,
11
+ TitleBlock,
12
+ )
13
+ from .entities import Entity, EntityLocation, Fact
14
+ from .projects import JobStatus, JobStep, Project, Sheet, SheetResult
15
+ from .search import (
16
+ EntitySummary,
17
+ GraphContext,
18
+ QueryResponse,
19
+ QuerySource,
20
+ RelationshipSummary,
21
+ SearchFilters,
22
+ SearchHit,
23
+ SearchResponse,
24
+ )
25
+
26
+ __all__ = [
27
+ # Common
28
+ "Point",
29
+ "BBox",
30
+ "TextSpan",
31
+ "Dimensions",
32
+ # Drawings (Tier 1)
33
+ "DrawingResult",
34
+ "Annotations",
35
+ "Leader",
36
+ "SectionTag",
37
+ "DetailTag",
38
+ "RevisionTriangle",
39
+ "RevisionCloud",
40
+ "TitleBlock",
41
+ # Projects (Tier 2)
42
+ "Project",
43
+ "Sheet",
44
+ "JobStatus",
45
+ "JobStep",
46
+ "SheetResult",
47
+ # Search
48
+ "SearchFilters",
49
+ "SearchHit",
50
+ "SearchResponse",
51
+ "EntitySummary",
52
+ "GraphContext",
53
+ "RelationshipSummary",
54
+ "QueryResponse",
55
+ "QuerySource",
56
+ # Entities
57
+ "Entity",
58
+ "EntityLocation",
59
+ "Fact",
60
+ ]
@@ -0,0 +1,36 @@
1
+ """Common types shared across models."""
2
+ from typing import List, Tuple
3
+
4
+ from pydantic import BaseModel
5
+
6
+ # Coordinate types
7
+ Point = Tuple[float, float] # [x, y]
8
+ BBox = Tuple[float, float, float, float] # [x1, y1, x2, y2]
9
+
10
+
11
+ class TextSpan(BaseModel):
12
+ """Text detected inside an annotation."""
13
+
14
+ id: int
15
+ text: str
16
+
17
+
18
+ class Dimensions(BaseModel):
19
+ """Page dimensions in pixels."""
20
+
21
+ width: int
22
+ height: int
23
+
24
+
25
+ class Circle(BaseModel):
26
+ """Circle geometry."""
27
+
28
+ center: Point
29
+ radius: float
30
+
31
+
32
+ class Line(BaseModel):
33
+ """Line segment."""
34
+
35
+ start: Point
36
+ end: Point
@@ -0,0 +1,81 @@
1
+ """Tier 1: Raw Detection models."""
2
+ from typing import List, Optional
3
+
4
+ from pydantic import BaseModel
5
+
6
+ from .common import BBox, Circle, Dimensions, Line, Point, TextSpan
7
+
8
+
9
+ class Leader(BaseModel):
10
+ """Leader annotation with arrow and text."""
11
+
12
+ id: str
13
+ bbox: BBox
14
+ arrow_tip: Point
15
+ text_bbox: BBox
16
+ texts_inside: List[TextSpan]
17
+
18
+
19
+ class SectionTag(BaseModel):
20
+ """Section cut tag."""
21
+
22
+ id: str
23
+ bbox: BBox
24
+ circle: Circle
25
+ direction: str # "left", "right", "up", "down"
26
+ texts_inside: List[TextSpan]
27
+ section_line: Optional[Line] = None
28
+
29
+
30
+ class DetailTag(BaseModel):
31
+ """Detail callout tag."""
32
+
33
+ id: str
34
+ bbox: BBox
35
+ circle: Circle
36
+ texts_inside: List[TextSpan]
37
+ has_dashed_bbox: bool = False
38
+
39
+
40
+ class RevisionTriangle(BaseModel):
41
+ """Revision marker triangle."""
42
+
43
+ id: str
44
+ bbox: BBox
45
+ vertices: List[Point]
46
+ text: str
47
+
48
+
49
+ class RevisionCloud(BaseModel):
50
+ """Revision cloud boundary."""
51
+
52
+ id: str
53
+ bbox: BBox
54
+
55
+
56
+ class Annotations(BaseModel):
57
+ """All detected annotations."""
58
+
59
+ leaders: List[Leader] = []
60
+ section_tags: List[SectionTag] = []
61
+ detail_tags: List[DetailTag] = []
62
+ revision_triangles: List[RevisionTriangle] = []
63
+ revision_clouds: List[RevisionCloud] = []
64
+
65
+
66
+ class TitleBlock(BaseModel):
67
+ """Title block detection."""
68
+
69
+ bounds: BBox
70
+ viewport: BBox # Drawing area excluding title block
71
+
72
+
73
+ class DrawingResult(BaseModel):
74
+ """Result from Tier 1 raw detection."""
75
+
76
+ id: str
77
+ page: int
78
+ dimensions: Dimensions
79
+ processing_ms: int
80
+ annotations: Annotations
81
+ titleblock: Optional[TitleBlock] = None
@@ -0,0 +1,37 @@
1
+ """Entity and Relationship models."""
2
+ from typing import List, Optional
3
+
4
+ from pydantic import BaseModel
5
+
6
+
7
+ class EntityLocation(BaseModel):
8
+ """Where an entity appears."""
9
+
10
+ sheet_id: str
11
+ sheet_title: Optional[str] = None
12
+ page: int
13
+
14
+
15
+ class Fact(BaseModel):
16
+ """Relationship between entities."""
17
+
18
+ id: str
19
+ fact: str
20
+ edge_type: str
21
+ source_id: str
22
+ source_label: Optional[str] = None
23
+ target_id: str
24
+ target_label: Optional[str] = None
25
+
26
+
27
+ class Entity(BaseModel):
28
+ """Full entity with relationships."""
29
+
30
+ id: str
31
+ type: str
32
+ label: str
33
+ description: Optional[str] = None
34
+ group_id: str
35
+ outgoing_facts: List[Fact] = []
36
+ incoming_facts: List[Fact] = []
37
+ locations: List[EntityLocation] = []
@@ -0,0 +1,83 @@
1
+ """Tier 2: Project and Sheet models."""
2
+ from datetime import datetime
3
+ from enum import Enum
4
+ from typing import Optional
5
+
6
+ from pydantic import BaseModel
7
+
8
+
9
+ class Project(BaseModel):
10
+ """Project container for sheets."""
11
+
12
+ id: str
13
+ name: str
14
+ description: Optional[str] = None
15
+ created_at: datetime
16
+ sheets_count: int = 0
17
+ entities_count: int = 0
18
+
19
+
20
+ class Sheet(BaseModel):
21
+ """Sheet in a project."""
22
+
23
+ id: str
24
+ title: Optional[str] = None
25
+ name: Optional[str] = None
26
+ page: int
27
+ width: int
28
+ height: int
29
+ created_at: datetime
30
+ entities_count: int = 0
31
+
32
+
33
+ class StepStatus(str, Enum):
34
+ """Status of a pipeline step."""
35
+
36
+ PENDING = "pending"
37
+ RUNNING = "running"
38
+ COMPLETE = "complete"
39
+ FAILED = "failed"
40
+
41
+
42
+ class JobStep(BaseModel):
43
+ """Status of a single pipeline step."""
44
+
45
+ status: StepStatus
46
+ duration_ms: Optional[int] = None
47
+ tokens: Optional[int] = None
48
+ error: Optional[str] = None
49
+
50
+
51
+ class JobSteps(BaseModel):
52
+ """All pipeline steps."""
53
+
54
+ detection: JobStep
55
+ enrichment: JobStep
56
+ synthesis: JobStep
57
+ graph: JobStep
58
+
59
+
60
+ class SheetResult(BaseModel):
61
+ """Result of sheet ingestion."""
62
+
63
+ sheet_id: str
64
+ entities_created: int
65
+ relationships_created: int
66
+
67
+
68
+ class JobStatus(BaseModel):
69
+ """Current job status."""
70
+
71
+ job_id: str
72
+ status: str # "processing", "complete", "failed"
73
+ steps: JobSteps
74
+ result: Optional[SheetResult] = None
75
+ error: Optional[str] = None
76
+
77
+ @property
78
+ def is_complete(self) -> bool:
79
+ return self.status == "complete"
80
+
81
+ @property
82
+ def is_failed(self) -> bool:
83
+ return self.status == "failed"
@@ -0,0 +1,70 @@
1
+ """Search and Query models."""
2
+ from typing import List, Optional
3
+
4
+ from pydantic import BaseModel
5
+
6
+ from .common import BBox
7
+
8
+
9
+ class SearchFilters(BaseModel):
10
+ """Filters for search."""
11
+
12
+ sheet_id: Optional[str] = None
13
+ entity_type: Optional[List[str]] = None
14
+
15
+
16
+ class EntitySummary(BaseModel):
17
+ """Brief entity info in search results."""
18
+
19
+ id: str
20
+ type: str
21
+ label: str
22
+ description: Optional[str] = None
23
+ sheet_id: str
24
+ bbox: Optional[BBox] = None
25
+
26
+
27
+ class RelationshipSummary(BaseModel):
28
+ """Brief relationship info."""
29
+
30
+ type: str
31
+ fact: str
32
+
33
+
34
+ class GraphContext(BaseModel):
35
+ """Graph context for a search result."""
36
+
37
+ connected_entities: List[EntitySummary]
38
+ relationships: List[RelationshipSummary]
39
+
40
+
41
+ class SearchHit(BaseModel):
42
+ """Single search result."""
43
+
44
+ entity: EntitySummary
45
+ score: float
46
+ graph_context: Optional[GraphContext] = None
47
+
48
+
49
+ class SearchResponse(BaseModel):
50
+ """Search response."""
51
+
52
+ results: List[SearchHit]
53
+ search_ms: int
54
+
55
+
56
+ class QuerySource(BaseModel):
57
+ """Source citation for query answer."""
58
+
59
+ entity_id: str
60
+ sheet_id: str
61
+ label: str
62
+ bbox: Optional[BBox] = None
63
+
64
+
65
+ class QueryResponse(BaseModel):
66
+ """Natural language query response."""
67
+
68
+ answer: str
69
+ sources: List[QuerySource]
70
+ confidence: float
struai/py.typed ADDED
File without changes
@@ -0,0 +1,5 @@
1
+ """API resources."""
2
+ from .drawings import AsyncDrawings, Drawings
3
+ from .projects import AsyncProjects, Projects
4
+
5
+ __all__ = ["Drawings", "AsyncDrawings", "Projects", "AsyncProjects"]
@@ -0,0 +1,122 @@
1
+ """Tier 1: Raw Detection API."""
2
+ from pathlib import Path
3
+ from typing import TYPE_CHECKING, BinaryIO, Union
4
+
5
+ from ..models.drawings import DrawingResult
6
+
7
+ if TYPE_CHECKING:
8
+ from .._base import AsyncBaseClient, BaseClient
9
+
10
+
11
+ class Drawings:
12
+ """Tier 1: Raw detection API (sync)."""
13
+
14
+ def __init__(self, client: "BaseClient"):
15
+ self._client = client
16
+
17
+ def analyze(
18
+ self,
19
+ file: Union[str, Path, bytes, BinaryIO],
20
+ page: int,
21
+ ) -> DrawingResult:
22
+ """Analyze a PDF page for annotations.
23
+
24
+ Args:
25
+ file: PDF file path, bytes, or file-like object
26
+ page: Page number (1-indexed)
27
+
28
+ Returns:
29
+ DrawingResult with detected annotations
30
+
31
+ Example:
32
+ >>> result = client.drawings.analyze("structural.pdf", page=4)
33
+ >>> for leader in result.annotations.leaders:
34
+ ... print(leader.texts_inside[0].text)
35
+ """
36
+ files = self._prepare_file(file)
37
+ return self._client.post(
38
+ "/drawings",
39
+ files=files,
40
+ data={"page": str(page)},
41
+ cast_to=DrawingResult,
42
+ )
43
+
44
+ def get(self, drawing_id: str) -> DrawingResult:
45
+ """Retrieve a previously processed drawing.
46
+
47
+ Args:
48
+ drawing_id: Drawing ID (e.g., "drw_7f8a9b2c")
49
+ """
50
+ return self._client.get(f"/drawings/{drawing_id}", cast_to=DrawingResult)
51
+
52
+ def delete(self, drawing_id: str) -> None:
53
+ """Delete a drawing result.
54
+
55
+ Args:
56
+ drawing_id: Drawing ID to delete
57
+ """
58
+ self._client.delete(f"/drawings/{drawing_id}")
59
+
60
+ def _prepare_file(self, file: Union[str, Path, bytes, BinaryIO]) -> dict:
61
+ """Prepare file for upload."""
62
+ if isinstance(file, (str, Path)):
63
+ path = Path(file)
64
+ return {"file": (path.name, open(path, "rb"), "application/pdf")}
65
+ elif isinstance(file, bytes):
66
+ return {"file": ("document.pdf", file, "application/pdf")}
67
+ else:
68
+ # File-like object
69
+ name = getattr(file, "name", "document.pdf")
70
+ if hasattr(name, "split"):
71
+ name = Path(name).name
72
+ return {"file": (name, file, "application/pdf")}
73
+
74
+
75
+ class AsyncDrawings:
76
+ """Tier 1: Raw detection API (async)."""
77
+
78
+ def __init__(self, client: "AsyncBaseClient"):
79
+ self._client = client
80
+
81
+ async def analyze(
82
+ self,
83
+ file: Union[str, Path, bytes, BinaryIO],
84
+ page: int,
85
+ ) -> DrawingResult:
86
+ """Analyze a PDF page for annotations (async).
87
+
88
+ Args:
89
+ file: PDF file path, bytes, or file-like object
90
+ page: Page number (1-indexed)
91
+
92
+ Returns:
93
+ DrawingResult with detected annotations
94
+ """
95
+ files = self._prepare_file(file)
96
+ return await self._client.post(
97
+ "/drawings",
98
+ files=files,
99
+ data={"page": str(page)},
100
+ cast_to=DrawingResult,
101
+ )
102
+
103
+ async def get(self, drawing_id: str) -> DrawingResult:
104
+ """Retrieve a previously processed drawing (async)."""
105
+ return await self._client.get(f"/drawings/{drawing_id}", cast_to=DrawingResult)
106
+
107
+ async def delete(self, drawing_id: str) -> None:
108
+ """Delete a drawing result (async)."""
109
+ await self._client.delete(f"/drawings/{drawing_id}")
110
+
111
+ def _prepare_file(self, file: Union[str, Path, bytes, BinaryIO]) -> dict:
112
+ """Prepare file for upload."""
113
+ if isinstance(file, (str, Path)):
114
+ path = Path(file)
115
+ return {"file": (path.name, open(path, "rb"), "application/pdf")}
116
+ elif isinstance(file, bytes):
117
+ return {"file": ("document.pdf", file, "application/pdf")}
118
+ else:
119
+ name = getattr(file, "name", "document.pdf")
120
+ if hasattr(name, "split"):
121
+ name = Path(name).name
122
+ return {"file": (name, file, "application/pdf")}