Skip to content

Commit 47dd71d

Browse files
committed
feat: use langchain for generic llm usage
1 parent f616593 commit 47dd71d

20 files changed

+556
-63
lines changed

requirements.txt

+7
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,12 @@ anthropic
5555
redis[hiredis]==5.0.3
5656
uwsgi==2.0.25.1
5757
psycopg2-binary==2.9.9
58+
sqlalchemy_easy_softdelete
59+
sqlalchemy_serializer
60+
pyruvate
61+
langchain
62+
langfuse
63+
langchain-openai
64+
langchain-anthropic
5865

5966
-e .

src/riskmatrix/__init__.py

+27-11
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
from riskmatrix.security_policy import SessionSecurityPolicy
1515
from openai import OpenAI
1616
from anthropic import Anthropic
17-
17+
from langchain_openai import ChatOpenAI
18+
from langchain_anthropic import ChatAnthropic
1819

1920
from typing import TYPE_CHECKING
2021
if TYPE_CHECKING:
@@ -87,31 +88,46 @@ def main(
8788
enable_tracing=True,
8889
send_default_pii=True
8990
)
90-
print("configured sentry")
91-
print(sentry_dsn)
9291

9392
with Configurator(settings=settings, root_factory=root_factory) as config:
9493
includeme(config)
9594

9695
if openai_apikey := settings.get('openai_api_key'):
97-
98-
openai_client = OpenAI(
99-
api_key=openai_apikey
96+
openai_client = ChatOpenAI(
97+
api_key=openai_apikey,
98+
model = "gpt-4o-mini",
99+
temperature=0.7
100100
)
101101
config.add_request_method(
102102
lambda r: openai_client,
103-
'openai',
103+
'llm',
104104
reify=True
105105
)
106-
if anthropic_apikey := settings.get('anthropic_api_key'):
107-
anthropic_client = Anthropic(
108-
api_key=anthropic_apikey
106+
elif anthropic_apikey := settings.get('anthropic_api_key'):
107+
anthropic_client = ChatAnthropic(
108+
api_key=anthropic_apikey,
109+
model="claude-3-5-sonnet-20240620",
110+
temperature=0.7
109111
)
110112
config.add_request_method(
111113
lambda r: anthropic_client,
112-
'anthropic',
114+
'llm',
115+
reify=True
116+
)
117+
118+
if langfuse_host := settings.get("langfuse_host"):
119+
from langfuse.callback import CallbackHandler
120+
langfuse_handler = CallbackHandler(
121+
secret_key=settings.get("langfuse_secret_key"),
122+
public_key=settings.get("langfuse_public_key"),
123+
host=langfuse_host,
124+
)
125+
config.add_request_method(
126+
lambda r: langfuse_handler,
127+
'langfuse',
113128
reify=True
114129
)
130+
115131

116132
app = config.make_wsgi_app()
117133
return Fanstatic(app, versioning=True)

src/riskmatrix/layouts/steps.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
from markupsafe import Markup
12
from riskmatrix.i18n import _
23

34
from typing import NamedTuple
45
from typing import TYPE_CHECKING
6+
7+
from riskmatrix.models.risk_assessment import RiskAssessment
8+
from riskmatrix.models.risk_assessment_info import RiskAssessmentInfo, RiskAssessmentState
59
if TYPE_CHECKING:
610
from pyramid.interfaces import IRequest
711

@@ -16,6 +20,12 @@ class Step(NamedTuple):
1620

1721

1822
def steps(context: 'Organization', request: 'IRequest') -> 'RenderData':
23+
assessments = request.dbsession.query(RiskAssessmentInfo).filter(
24+
RiskAssessmentInfo.organization_id == context.id,
25+
RiskAssessmentInfo.state == RiskAssessmentState.OPEN,
26+
).all()
27+
28+
t = request.dbsession.query(RiskAssessment).filter(RiskAssessment.risk_assessment_info_id.in_([a.id for a in assessments]), RiskAssessment.likelihood == None, RiskAssessment.impact == None).count()
1929
return {
2030
'steps': [
2131
Step(
@@ -35,10 +45,11 @@ def steps(context: 'Organization', request: 'IRequest') -> 'RenderData':
3545
request.route_url('generate_risk_matrix')
3646
),
3747
Step(
38-
_('Plan Actions'),
48+
Markup('<s>Plan Actions</s>'),
3949
'#',
4050
disabled=True
4151
),
52+
Step(_("Finish Assessment"), request.route_url('finish_assessment'), disabled=False),#t > 0 or len(assessments) == 0),
4253
]
4354
}
4455

@@ -54,5 +65,6 @@ def show_steps(request: 'IRequest') -> bool:
5465
'assess_impact',
5566
'assess_likelihood',
5667
'generate_risk_matrix',
68+
'finish_assessment'
5769
)
5870
return False

src/riskmatrix/models/__init__.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from .risk_assessment import RiskAssessment, RiskMatrixAssessment
1313
from .risk_catalog import RiskCatalog
1414
from .risk_category import RiskCategory
15+
from .risk_assessment_info import RiskAssessmentInfo
1516
from .user import User
1617
from .password_change_token import PasswordChangeToken
1718

@@ -63,5 +64,6 @@ def includeme(config: 'Configurator') -> None:
6364
'RiskCategory',
6465
'RiskMatrixAssessment',
6566
'User',
66-
'PasswordChangeToken'
67+
'PasswordChangeToken',
68+
'RiskAssessmentInfo',
6769
)

src/riskmatrix/models/asset.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import datetime
22
from pyramid.authorization import Allow
33
from sedate import utcnow
4+
from riskmatrix.orm.softdelete_base import SoftDeleteMixin
45
from sqlalchemy import ForeignKey
56
from sqlalchemy import UniqueConstraint
67
from sqlalchemy.ext.hybrid import hybrid_property
@@ -25,8 +26,9 @@
2526
from riskmatrix.models import RiskAssessment
2627
from riskmatrix.types import ACL
2728

29+
from sqlalchemy_serializer import SerializerMixin
2830

29-
class Asset(Base):
31+
class Asset(Base, SoftDeleteMixin, SerializerMixin):
3032

3133
__tablename__ = 'asset'
3234
__table_args__ = (
@@ -48,6 +50,7 @@ class Asset(Base):
4850
assessments: Mapped[list['RiskAssessment']] = relationship(
4951
back_populates='asset'
5052
)
53+
5154
organization: Mapped['Organization'] = relationship(
5255
back_populates='assets'
5356
)

src/riskmatrix/models/risk.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import datetime
22
from pyramid.authorization import Allow
33
from sedate import utcnow
4+
from riskmatrix.orm.softdelete_base import SoftDeleteMixin
45
from sqlalchemy import ForeignKey
56
from sqlalchemy import ForeignKeyConstraint
67
from sqlalchemy import UniqueConstraint
@@ -25,8 +26,9 @@
2526
from riskmatrix.models import RiskCatalog
2627
from riskmatrix.types import ACL
2728

29+
from sqlalchemy_serializer import SerializerMixin
2830

29-
class Risk(Base):
31+
class Risk(SoftDeleteMixin, Base, SerializerMixin):
3032

3133
__tablename__ = 'risk'
3234
__table_args__ = (

src/riskmatrix/models/risk_assessment.py

+27-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import datetime
22
from pyramid.authorization import Allow
33
from sedate import utcnow
4+
from riskmatrix.orm.softdelete_base import SoftDeleteMixin
45
from sqlalchemy import ForeignKey
56
from sqlalchemy import UniqueConstraint
67
from sqlalchemy.ext.hybrid import hybrid_property
@@ -12,22 +13,25 @@
1213

1314
from riskmatrix.models import Asset
1415
from riskmatrix.models import Risk
16+
from riskmatrix.models.risk_assessment_info import RiskAssessmentInfo
1517
from riskmatrix.orm.meta import Base
1618
from riskmatrix.orm.meta import UUIDStr
1719
from riskmatrix.orm.meta import UUIDStrPK
18-
20+
from sqlalchemy.types import JSON
21+
from dataclasses import dataclass
22+
from sqlalchemy_serializer import SerializerMixin
1923

2024
from typing import Any, ClassVar
2125
from typing import TYPE_CHECKING
2226
if TYPE_CHECKING:
2327
from riskmatrix.types import ACL
2428

2529

26-
class RiskAssessment(Base):
30+
class RiskAssessment(SoftDeleteMixin, Base, SerializerMixin):
2731

2832
__tablename__ = 'risk_assessment'
2933
__table_args__ = (
30-
UniqueConstraint('risk_id', 'asset_id'),
34+
UniqueConstraint('risk_id', 'asset_id', 'risk_assessment_info_id'),
3135
)
3236

3337
id: Mapped[UUIDStrPK]
@@ -40,33 +44,53 @@ class RiskAssessment(Base):
4044
index=True,
4145
)
4246

47+
risk_assessment_info_id: Mapped[UUIDStr] = mapped_column(
48+
ForeignKey('risk_assessment_info.id', ondelete='CASCADE'),
49+
index=True,
50+
)
51+
4352
meta: Mapped[dict[str, Any]] = mapped_column(default={})
4453
impact: Mapped[int | None]
4554
likelihood: Mapped[int | None]
4655

4756
created: Mapped[datetime] = mapped_column(default=utcnow)
4857
modified: Mapped[datetime | None] = mapped_column(onupdate=utcnow)
4958

59+
state_at_finish: Mapped[JSON | None] = mapped_column(JSON, nullable=True)
60+
5061
risk: Mapped[Risk] = relationship(
5162
back_populates='assessments',
5263
lazy='joined'
5364
)
65+
5466
asset: Mapped[Asset] = relationship(
5567
back_populates='assessments',
5668
lazy='joined'
5769
)
5870

71+
risk_assessment_info: Mapped[RiskAssessmentInfo] = relationship(
72+
back_populates='assessments',
73+
lazy='joined'
74+
)
75+
76+
risk_assessment_info_id: Mapped[UUIDStr] = mapped_column(
77+
ForeignKey('risk_assessment_info.id', ondelete='CASCADE'),
78+
index=True,
79+
)
80+
5981
def __init__(
6082
self,
6183
asset: Asset,
6284
risk: Risk,
85+
info: 'RiskAssessmentInfo',
6386
**meta: Any
6487
):
6588
self.id = str(uuid4())
6689
self.created = utcnow()
6790
self.asset = asset
6891
self.risk = risk
6992
self.meta = meta
93+
self.risk_assessment_info = info
7094

7195
@validates('impact', 'likelihood')
7296
def ensure_larger_than_one(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from dataclasses import dataclass
2+
from sedate import utcnow
3+
from riskmatrix.orm.meta import Base
4+
import enum
5+
from sqlalchemy import Enum, ForeignKey
6+
from sqlalchemy.orm import mapped_column
7+
from sqlalchemy.orm import relationship
8+
from sqlalchemy.orm import Mapped
9+
from riskmatrix.orm.meta import UUIDStr
10+
from riskmatrix.orm.meta import UUIDStrPK
11+
from sqlalchemy import UniqueConstraint
12+
from sqlalchemy import Column
13+
from datetime import datetime
14+
15+
from typing import TYPE_CHECKING
16+
17+
if TYPE_CHECKING:
18+
from riskmatrix.models import RiskAssessment
19+
20+
21+
22+
23+
class RiskAssessmentState(enum.Enum):
24+
OPEN = 0
25+
FINISHED = 1
26+
27+
def __str__(self) -> str:
28+
names = {
29+
RiskAssessmentState.OPEN: "Offen",
30+
RiskAssessmentState.FINISHED: "Geschlossen"
31+
}
32+
return names[self]
33+
34+
from sqlalchemy_serializer import SerializerMixin
35+
36+
37+
class RiskAssessmentInfo(Base, SerializerMixin):
38+
39+
__tablename__ = 'risk_assessment_info'
40+
41+
id: Mapped[UUIDStrPK] = mapped_column()
42+
organization_id: Mapped[UUIDStr] = mapped_column(
43+
ForeignKey('organization.id', ondelete='CASCADE'),
44+
index=True,
45+
)
46+
47+
name: Mapped[str] = mapped_column(nullable=True)
48+
49+
state: Mapped[RiskAssessmentState] = mapped_column(
50+
Enum(RiskAssessmentState),
51+
default=RiskAssessmentState.OPEN,
52+
)
53+
54+
created: Mapped[datetime] = mapped_column(default=utcnow)
55+
modified: Mapped[datetime | None] = mapped_column(onupdate=utcnow)
56+
57+
finished_at: Mapped[datetime | None] = mapped_column()
58+
59+
assessments: Mapped[list['RiskAssessment']] = relationship(
60+
back_populates='risk_assessment_info'
61+
)
62+
63+
def __init__(self, organization_id: str):
64+
self.organization_id = organization_id
65+
self.state = RiskAssessmentState.OPEN
66+
self.finished_at = None

src/riskmatrix/models/risk_catalog.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import datetime
22
from pyramid.authorization import Allow
33
from sedate import utcnow
4+
from riskmatrix.orm.softdelete_base import SoftDeleteMixin
45
from sqlalchemy import ForeignKey
56
from sqlalchemy import UniqueConstraint
67
from sqlalchemy.orm import mapped_column
@@ -15,6 +16,7 @@
1516
from riskmatrix.orm.meta import UUIDStr
1617
from riskmatrix.orm.meta import UUIDStrPK
1718

19+
from sqlalchemy_serializer import SerializerMixin
1820

1921
from typing import TYPE_CHECKING
2022
if TYPE_CHECKING:
@@ -23,7 +25,7 @@
2325
from riskmatrix.types import ACL
2426

2527

26-
class RiskCatalog(Base):
28+
class RiskCatalog(Base, SoftDeleteMixin, SerializerMixin):
2729

2830
__tablename__ = 'risk_catalog'
2931
__table_args__ = (

src/riskmatrix/models/risk_category.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import datetime
22
from pyramid.authorization import Allow
33
from sedate import utcnow
4+
from riskmatrix.orm.softdelete_base import SoftDeleteMixin
45
from sqlalchemy import ForeignKey
56
from sqlalchemy import UniqueConstraint
67
from sqlalchemy.orm import mapped_column
@@ -22,7 +23,7 @@
2223
from riskmatrix.types import ACL
2324

2425

25-
class RiskCategory(Base):
26+
class RiskCategory(Base, SoftDeleteMixin):
2627

2728
__tablename__ = 'risk_category'
2829
__table_args__ = (

0 commit comments

Comments
 (0)