Skip to content

Commit f983b2d

Browse files
authored
Issue LIF-Initiative#747: Fix issue with GraphQL failing due to getting null values… (LIF-Initiative#753)
… for required fields <!-- Thank you for your pull request. Please review the requirements below. Bug fixes and new features should be reported on the issue tracker: https://github.com/lif-initiative/lif-core/issues Contributing guide: https://github.com/lif-initiative/lif-core/blob/main/docs/CONTRIBUTING.md Code of Conduct: https://github.com/lif-initiative/lif-core/blob/main/CODE_OF_CONDUCT.md --> ##### Checklist <!-- Remove items that do not apply. For completed items, change [ ] to [x]. --> - [x] commit message follows commit guidelines (see commitlint.config.mjs) - [x] tests are included (unit and/or integration tests) - [x] all tests are successful - [x] documentation is changed or added (in /docs directory) - [x] code passes linting checks (`uv run ruff check`) - [x] code passes formatting checks (`uv run ruff format`) - [x] code passes type checking (`uv run ty check`) - [x] pre-commit hooks have been run successfully - [ ] database schema changes: migration files created and CHANGELOG.md updated - [ ] API changes: base (Python code) documentation in `docs/` and project README updated - [ ] configuration changes: relevant folder README updated - [ ] breaking changes: added to MIGRATION.md with upgrade instructions and CHANGELOG.md entry ##### Type of Change <!-- Check all that apply --> - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [x] Documentation update - [ ] Infrastructure/deployment change - [ ] Performance improvement - [x] Code refactoring ##### Description of Change <!-- Provide a clear and detailed description of the change below this comment. Include: - What problem does this solve? - What is the solution? - Are there any side effects or limitations? - How should reviewers test this? --> ##### Related Issues <!-- Link to related issues using #issue_number --> Closes LIF-Initiative#747 ##### Testing <!-- Describe the testing you've done --> - [ ] Manual testing performed - [ ] Automated tests added/updated - [ ] Integration testing completed ##### Project Area(s) Affected <!-- Check all project areas affected by this change --> - [ ] bases/ - [ ] components/ - [ ] orchestrators/ - [ ] frontends/ - [ ] deployments/ - [ ] CloudFormation/SAM templates - [ ] Database schema - [ ] API endpoints - [ ] Documentation - [ ] Testing ##### Additional Notes <!-- Any additional information that reviewers should know -->
2 parents 4e959bc + b38f458 commit f983b2d

File tree

77 files changed

+19562
-6738
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+19562
-6738
lines changed

bases/lif/api_graphql/core.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,28 @@
1111
from fastapi import FastAPI
1212
from strawberry.fastapi import GraphQLRouter
1313

14+
from lif.lif_schema_config import LIFSchemaConfig
1415
from lif.logging import get_logger
1516
from lif.mdr_client import get_openapi_lif_data_model
1617
from lif.openapi_to_graphql.core import generate_graphql_schema
1718

1819
logger = get_logger(__name__)
1920

2021

21-
LIF_QUERY_PLANNER_URL = os.getenv("LIF_QUERY_PLANNER_URL", "http://localhost:8002")
22-
LIF_GRAPHQL_ROOT_TYPE_NAME = os.getenv("LIF_GRAPHQL_ROOT_TYPE_NAME", "Person")
22+
# Load centralized configuration from environment
23+
CONFIG = LIFSchemaConfig.from_environment()
2324

24-
logger.info(f"LIF_QUERY_PLANNER_URL: {LIF_QUERY_PLANNER_URL}")
25-
logger.info(f"LIF_GRAPHQL_ROOT_TYPE_NAME: {LIF_GRAPHQL_ROOT_TYPE_NAME}")
26-
logger.info(f"LIF_MDR_API_URL: {os.getenv('LIF_MDR_API_URL')}")
25+
logger.info(f"LIF_QUERY_PLANNER_URL: {CONFIG.query_planner_base_url}")
26+
logger.info(f"LIF_GRAPHQL_ROOT_TYPE_NAME: {CONFIG.root_type_name}")
27+
logger.info(f"LIF_MDR_API_URL: {CONFIG.mdr_api_url}")
2728

2829

2930
async def fetch_dynamic_graphql_schema(openapi: dict):
3031
return await generate_graphql_schema(
3132
openapi=openapi,
32-
root_type_name=LIF_GRAPHQL_ROOT_TYPE_NAME,
33-
query_planner_query_url=LIF_QUERY_PLANNER_URL.rstrip("/") + "/query",
34-
query_planner_update_url=LIF_QUERY_PLANNER_URL.rstrip("/") + "/update",
33+
root_type_name=CONFIG.root_type_name,
34+
query_planner_query_url=CONFIG.query_planner_query_url,
35+
query_planner_update_url=CONFIG.query_planner_update_url,
3536
)
3637

3738

bases/lif/semantic_search_mcp_server/core.py

Lines changed: 64 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from starlette.requests import Request
1010
from starlette.responses import PlainTextResponse
1111

12+
from lif.lif_schema_config import LIFSchemaConfig, DEFAULT_ATTRIBUTE_KEYS
1213
from lif.logging import get_logger
1314
from lif.mdr_client import get_openapi_lif_data_model_from_file
1415
from lif.openapi_schema_parser import load_schema_leaves
@@ -23,41 +24,73 @@
2324
logger = get_logger(__name__)
2425

2526

26-
# --------- LOAD ENVIRONMENT VARIABLES ---------
27+
# --------- LOAD CONFIGURATION ---------
2728

28-
ROOT_NODE = os.getenv("LIF_GRAPHQL_ROOT_NODE", "Person")
29+
# Load centralized configuration from environment
30+
CONFIG = LIFSchemaConfig.from_environment()
31+
32+
# Extract values for convenience
33+
ROOT_NODES = CONFIG.all_root_types
34+
DEFAULT_ROOT_NODE = CONFIG.root_type_name
2935
LIF_GRAPHQL_API_URL = os.getenv("LIF_GRAPHQL_API_URL", "http://localhost:8002/graphql")
30-
TOP_K = int(os.getenv("TOP_K", 200))
31-
MODEL_NAME = "all-MiniLM-L6-v2"
36+
TOP_K = CONFIG.semantic_search_top_k
37+
MODEL_NAME = CONFIG.semantic_search_model_name
3238

33-
ATTRIBUTE_KEYS = ["x-queryable", "x-mutable", "DataType", "Required", "Array", "enum"] # Add more attributes as needed
39+
ATTRIBUTE_KEYS = DEFAULT_ATTRIBUTE_KEYS
3440

3541

3642
# --------- LOAD VECTOR STORE & EMBEDDINGS ---------
3743

3844
# get the OpenAPI specification for the LIF data model from the MDR
3945
openapi = get_openapi_lif_data_model_from_file()
4046

41-
try:
42-
LEAVES = load_schema_leaves(openapi, ROOT_NODE, attribute_keys=ATTRIBUTE_KEYS)
43-
except Exception as e:
44-
logger.critical(f"Failed to load schema leaves: {e}")
45-
sys.exit(1)
46-
47-
try:
48-
FILTER = build_dynamic_filter_model(LEAVES)
49-
Filter = FILTER[ROOT_NODE]
50-
logger.info("Dynamic Filter Schema:\n" + json.dumps(Filter.model_json_schema(), indent=2))
51-
except Exception as e:
52-
logger.critical(f"Failed to load build dynamic filter model: {e}")
53-
sys.exit(1)
54-
55-
try:
56-
MUTATION_MODELS = build_dynamic_mutation_model(LEAVES)
57-
MutationModel = MUTATION_MODELS[ROOT_NODE]
58-
logger.info("Dynamic Mutation Schema:\n" + json.dumps(MutationModel.model_json_schema(), indent=2))
59-
except Exception as e:
60-
logger.critical(f"Failed to build dynamic mutation model: {e}")
47+
# Load schema leaves for all root nodes
48+
ALL_LEAVES: List = []
49+
LEAVES_BY_ROOT: dict = {}
50+
51+
for root_node in ROOT_NODES:
52+
try:
53+
root_leaves = load_schema_leaves(openapi, root_node, attribute_keys=ATTRIBUTE_KEYS)
54+
LEAVES_BY_ROOT[root_node] = root_leaves
55+
ALL_LEAVES.extend(root_leaves)
56+
logger.info(f"Loaded {len(root_leaves)} schema leaves for root '{root_node}'")
57+
except Exception as e:
58+
# Primary root (Person) is required; additional roots are optional
59+
if root_node == DEFAULT_ROOT_NODE:
60+
logger.critical(f"Failed to load schema leaves for required root '{root_node}': {e}")
61+
sys.exit(1)
62+
else:
63+
logger.warning(f"Failed to load schema leaves for optional root '{root_node}': {e}")
64+
65+
logger.info(f"Total schema leaves loaded: {len(ALL_LEAVES)}")
66+
67+
# Build filter and mutation models for each root
68+
FILTER_MODELS: dict = {}
69+
MUTATION_MODELS: dict = {}
70+
71+
for root_node, root_leaves in LEAVES_BY_ROOT.items():
72+
try:
73+
filter_model = build_dynamic_filter_model(root_leaves)
74+
if root_node in filter_model:
75+
FILTER_MODELS[root_node] = filter_model[root_node]
76+
logger.info(f"Dynamic Filter Schema for '{root_node}':\n" + json.dumps(filter_model[root_node].model_json_schema(), indent=2))
77+
except Exception as e:
78+
logger.warning(f"Failed to build dynamic filter model for '{root_node}': {e}")
79+
80+
try:
81+
mutation_model = build_dynamic_mutation_model(root_leaves)
82+
if root_node in mutation_model:
83+
MUTATION_MODELS[root_node] = mutation_model[root_node]
84+
logger.info(f"Dynamic Mutation Schema for '{root_node}':\n" + json.dumps(mutation_model[root_node].model_json_schema(), indent=2))
85+
except Exception as e:
86+
logger.warning(f"Failed to build dynamic mutation model for '{root_node}': {e}")
87+
88+
# Use default root for backwards compatibility
89+
Filter = FILTER_MODELS.get(DEFAULT_ROOT_NODE)
90+
MutationModel = MUTATION_MODELS.get(DEFAULT_ROOT_NODE)
91+
92+
if not Filter:
93+
logger.critical(f"Failed to build filter model for default root '{DEFAULT_ROOT_NODE}'")
6194
sys.exit(1)
6295

6396
try:
@@ -66,15 +99,18 @@
6699
logger.critical(f"Failed to load SentenceTransformer model: {e}")
67100
sys.exit(1)
68101

69-
# ------ ALWAYS embed only the descriptions ------
70-
EMBEDDING_TEXTS = [leaf.description for leaf in LEAVES]
102+
# ------ ALWAYS embed only the descriptions for ALL leaves (all root entities) ------
103+
EMBEDDING_TEXTS = [leaf.description for leaf in ALL_LEAVES]
71104

72105
try:
73106
EMBEDDINGS = build_embeddings(EMBEDDING_TEXTS, MODEL)
74107
except Exception as e:
75108
logger.critical(f"EMBEDDINGS failed: {e}")
76109
sys.exit(1)
77110

111+
# Keep a reference to all leaves for search
112+
LEAVES = ALL_LEAVES
113+
78114

79115
mcp = FastMCP(name="LIF-Query-Server")
80116

@@ -99,6 +135,7 @@ async def lif_query(
99135
leaves=LEAVES,
100136
top_k=TOP_K,
101137
graphql_url=LIF_GRAPHQL_API_URL,
138+
config=CONFIG,
102139
)
103140

104141

cloudformation/lif-semantic-search-taskdef-includes.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,7 @@ Environment:
1717
Value: "openapi_constrained_with_interactions.json"
1818
- Name: USE_OPENAPI_DATA_MODEL_FROM_FILE
1919
Value: "false"
20+
Secrets:
21+
- Name: LIF_MDR_API_AUTH_TOKEN
22+
ValueFrom:
23+
Fn::Sub: "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${EnvironmentName}/${ServiceName}/MdrApiKey"

components/lif/composer/core.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,16 @@ def compose_with_fragment_list(lif_record: LIFRecord, lif_fragments: List[LIFFra
4141

4242

4343
def adjust_fragment_path_for_root_person_list(fragment_path: str) -> str:
44+
"""Adjust fragment path to navigate into the person array.
45+
46+
Handles both lowercase 'person.' and PascalCase 'Person.' prefixes.
47+
The internal LIF record structure uses lowercase 'person'.
48+
"""
4449
if fragment_path.startswith("person."):
4550
return "person.0" + fragment_path[6::]
51+
elif fragment_path.startswith("Person."):
52+
# Convert PascalCase to lowercase for internal navigation
53+
return "person.0" + fragment_path[6:]
4654
else:
4755
return fragment_path
4856

@@ -80,8 +88,8 @@ def add_fragment_items_to_list(list_to_update: list, new_items: list):
8088
logger.error(f"Expected a list but got: {type(list_to_update)}")
8189
raise ValueError("Expected a list to update")
8290
if not new_items:
83-
logger.error("No items to add to the list")
84-
raise ValueError("No items to add to the list")
91+
logger.warning("No items to add to the list, skipping")
92+
return
8593
for new_item in new_items:
8694
if isinstance(new_item, dict):
8795
list_to_update.append(new_item)
Lines changed: 46 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,33 @@
11
query GetPersonByIdentifier($identifier: String!, $identifierType: String!) {
2-
person(filter: { identifier: { identifier: $identifier, identifierType: $identifierType } }) {
3-
name {
2+
person(filter: { Identifier: { identifier: $identifier, identifierType: $identifierType } }) {
3+
Name {
44
informationSourceId
55
lastName
66
firstName
77
}
8-
contact {
9-
email {
8+
Contact {
9+
Email {
1010
informationSourceId
1111
emailAddress
1212
}
1313
}
14-
identifier {
14+
Identifier {
1515
informationSourceId
1616
identifier
1717
# identifierType
1818
}
19-
employmentLearningExperience {
19+
EmploymentLearningExperience {
2020
informationSourceId
2121
name
2222
startDate
2323
endDate
24-
#refPosition {
25-
# name
26-
# informationSourceId
27-
#}
28-
#assertedByRefOrganization {
29-
# identificationSystem
30-
# name
31-
# organizationType
32-
# informationSourceId
33-
#}
34-
#offeredByRefOrganization {
35-
# identificationSystem
36-
# name
37-
# organizationType
38-
# informationSourceId
39-
#}
40-
#approvedByRefOrganization {
41-
# identificationSystem
42-
# name
43-
# organizationType
44-
# informationSourceId
45-
#}
46-
refCredentialAward {
24+
RefPosition {
25+
identifier
26+
name
27+
informationSourceId
28+
informationSourceOrganization
29+
}
30+
RefCredentialAward {
4731
identifier
4832
#awardIssueDate
4933
#credentialAwardee
@@ -52,34 +36,34 @@ query GetPersonByIdentifier($identifier: String!, $identifierType: String!) {
5236
name
5337
}
5438
}
55-
positionPreferences {
39+
PositionPreferences {
5640
# informationSourceId
57-
travel {
41+
Travel {
5842
percentage
5943
willingToTravelIndicator
6044
}
61-
relocation {
45+
Relocation {
6246
willingToRelocateIndicator
6347
}
64-
remoteWork {
48+
RemoteWork {
6549
remoteWorkIndicator
6650
}
6751
positionTitles
6852
positionOfferingTypeCodes
6953
positionScheduleTypeCodes
70-
#offeredByRefRemunerationPackage {
71-
# ranges {
72-
# minimumAmount {
54+
#OfferedByRefRemunerationPackage {
55+
# Ranges {
56+
# MinimumAmount {
7357
# value
7458
# currency
7559
# }
7660
# }
7761
# basisCode
7862
#}
7963
}
80-
employmentPreferences {
64+
EmploymentPreferences {
8165
# informationSourceId
82-
# offeredByRefOrganization {
66+
# OfferedByRefOrganization {
8367
# informationSourceId
8468
# name
8569
# identificationSystem
@@ -89,11 +73,34 @@ query GetPersonByIdentifier($identifier: String!, $identifierType: String!) {
8973
organizationTypes
9074
organizationNames
9175
}
92-
credentialAward {
76+
CredentialAward {
9377
identifier
9478
awardIssueDate
9579
credentialAwardee
9680
informationSourceId
81+
InstanceOfRefCredential {
82+
informationSourceId
83+
informationSourceOrganization
84+
identifier
85+
name
86+
}
87+
}
88+
CourseLearningExperience {
89+
informationSourceId
90+
startDate
91+
endDate
92+
RefCourse {
93+
informationSourceId
94+
informationSourceOrganization
95+
identifier
96+
name
97+
}
98+
RefCredentialAward {
99+
informationSourceId
100+
informationSourceOrganization
101+
identifier
102+
name
103+
}
97104
}
98105
}
99106
}

0 commit comments

Comments
 (0)