struai 0.0.1__py3-none-any.whl → 0.2.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.
- struai/__init__.py +112 -2
- struai/_base.py +360 -0
- struai/_client.py +112 -0
- struai/_exceptions.py +103 -0
- struai/_version.py +2 -0
- struai/models/__init__.py +62 -0
- struai/models/common.py +36 -0
- struai/models/drawings.py +81 -0
- struai/models/entities.py +62 -0
- struai/models/projects.py +83 -0
- struai/models/search.py +70 -0
- struai/py.typed +0 -0
- struai/resources/__init__.py +5 -0
- struai/resources/drawings.py +122 -0
- struai/resources/projects.py +628 -0
- struai-0.2.0.dist-info/METADATA +151 -0
- struai-0.2.0.dist-info/RECORD +18 -0
- struai-0.0.1.dist-info/METADATA +0 -17
- struai-0.0.1.dist-info/RECORD +0 -4
- {struai-0.0.1.dist-info → struai-0.2.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
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, EntityListItem, EntityLocation, EntityRelation, 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
|
+
"EntityListItem",
|
|
59
|
+
"EntityLocation",
|
|
60
|
+
"EntityRelation",
|
|
61
|
+
"Fact",
|
|
62
|
+
]
|
struai/models/common.py
ADDED
|
@@ -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,62 @@
|
|
|
1
|
+
"""Entity and Relationship models."""
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from .common import BBox
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EntityLocation(BaseModel):
|
|
10
|
+
"""Where an entity appears."""
|
|
11
|
+
|
|
12
|
+
sheet_id: str
|
|
13
|
+
sheet_title: Optional[str] = None
|
|
14
|
+
page: Optional[int] = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EntityRelation(BaseModel):
|
|
18
|
+
"""Relationship entry on an entity detail response."""
|
|
19
|
+
|
|
20
|
+
uuid: str
|
|
21
|
+
type: str
|
|
22
|
+
fact: str
|
|
23
|
+
source_id: Optional[str] = None
|
|
24
|
+
source_label: Optional[str] = None
|
|
25
|
+
target_id: Optional[str] = None
|
|
26
|
+
target_label: Optional[str] = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Fact(BaseModel):
|
|
30
|
+
"""Relationship between entities (list endpoint)."""
|
|
31
|
+
|
|
32
|
+
id: str
|
|
33
|
+
type: str
|
|
34
|
+
fact: str
|
|
35
|
+
source_id: str
|
|
36
|
+
target_id: str
|
|
37
|
+
source_label: Optional[str] = None
|
|
38
|
+
target_label: Optional[str] = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class EntityListItem(BaseModel):
|
|
42
|
+
"""Entity summary from list endpoint."""
|
|
43
|
+
|
|
44
|
+
id: str
|
|
45
|
+
type: str
|
|
46
|
+
label: str
|
|
47
|
+
description: Optional[str] = None
|
|
48
|
+
sheet_id: Optional[str] = None
|
|
49
|
+
bbox: Optional[BBox] = None
|
|
50
|
+
attributes: Optional[str] = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Entity(BaseModel):
|
|
54
|
+
"""Full entity with relationships."""
|
|
55
|
+
|
|
56
|
+
id: str
|
|
57
|
+
type: str
|
|
58
|
+
label: str
|
|
59
|
+
description: Optional[str] = None
|
|
60
|
+
outgoing: List[EntityRelation] = []
|
|
61
|
+
incoming: List[EntityRelation] = []
|
|
62
|
+
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"
|
struai/models/search.py
ADDED
|
@@ -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,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")}
|